Sophisticated pseudorandom order

Hi All!

In my task, I am presenting 8 female and 8 male pictures followed by either a STEM-related or Arts-related word (10 words in each category).
There are 3 blocks with 120 trials in each block and following is the pseudorandom order for each block:

  1. 60 trials where female face is paired with STEM-related word
  2. 20 trials where females face is paired with Arts-related word
  3. 20 trials where male face is paired with STEM-related word
  4. 20 trials where male face is paired with Arts-related word

And these trials need to be presented in a random sequence across the 3 blocks.

Each image stays on screen for 1800 ms and at the 1000th ms, the STEM or Arts related word is presented and it lasts for 800 ms. During this 800 ms, participants have to press ENTER key for trials with female face + STEM-related word only. If they press ENTER for the correct trial, then a happy face will be presented for 1000 ms and if they press ENTER for any other trial an X will be presented for 1000 ms.
Intertrial interval is 1000 ms.

Could anyone please help me with the coding aspect of this task. I am not very familiar with programming on Python so your help is very much appreciated.

Mo

1 Like

@movivi, there is minimal coding needed for this experiment, most can all be made in Builder, using a image component, a keyboard component and a text component, in a loop with a conditions file that contains all your images, and a column for correct answers, which are fed into your keyboard component. If you can get that running, we can look at providing some feedback using a code component.

@dvbridges thank you for your prompt response. Following is the code from my progress in the builder version. Current caveats with the code are as follows:

  1. I need to press Spacebar first to view the first image, I don’t know why. I only allow enter key to be pressed during this routine.

  2. If I don’t press the Enter key before 1.8 seconds then the screen goes blank and the next image is not presented.

  3. In the current version, the same face and word is paired across multiple iteration. That is not desired, instead, the conditions needed is that in the block 60 trials should be female faces + STEM words and so on (as described in my first post).

  4. Due to unbalanced image vs. word stimuli (total images = 16 and total STEM/Arts word = 20) so I am not able to configure which rows are the correct response. Instead I used a column based criteria and I don’t think it’s the right way to programme it but I am not aware of any other option to tell the programme that whichever pair, except for Female face + STEM word, is wrong and a sad face needs to be shown as feedback. Even not responding within 800 ms also results in a sad face.

        # ------Prepare to start Routine "pr_bl"-------
      t = 0
      pr_blClock.reset()  # clock
      frameN = -1
      continueRoutine = True
       # update component parameters for each repeat
       face.setOpacity(1)
       face.setPos((0, 0))
       face.setSize((1, 1))
       face.setOri(0)
       face.setImage(female_faces or male_faces)
       word_pair.setColor('black', colorSpace='rgb')
       word_pair.setPos((0, -0.7))
       word_pair.setText(stem_words or arts_word)
       word_pair.setFont('Arial')
       word_pair.setHeight(0.1)
       press_enter = event.BuilderKeyResponse()
       happy_face.setOpacity(1)
       happy_face.setPos((0, 0))
       happy_face.setSize((0.7, 0.7))
        happy_face.setOri(0)
        happy_face.setImage(correct)
        sad_face.setOpacity(1)
        sad_face.setPos((0, 0))
        sad_face.setSize((0.7, 0.7))
        sad_face.setOri(0)
        sad_face.setImage(incorrect)
       # keep track of which components have finished
       pr_blComponents = [face, word_pair, press_enter, ITI, happy_face, sad_face]
       for thisComponent in pr_blComponents:
        if hasattr(thisComponent, 'status'):
         thisComponent.status = NOT_STARTED
    
         # -------Start Routine "pr_bl"-------
         while continueRoutine:
          # get current time
          t = pr_blClock.getTime()
          frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
           # update/draw components on each frame
     
           # *face* updates
           if t >= 0 and face.status == NOT_STARTED:
         # keep track of start time/frame for later
         face.tStart = t
         face.frameNStart = frameN  # exact frame index
         face.setAutoDraw(True)
          frameRemains = 0 + 1.8- win.monitorFramePeriod * 0.75  # most of one frame period left
     if face.status == STARTED and t >= frameRemains:
         face.setAutoDraw(False)
     
     # *word_pair* updates
     if t >= 1 and word_pair.status == NOT_STARTED:
         # keep track of start time/frame for later
         word_pair.tStart = t
         word_pair.frameNStart = frameN  # exact frame index
         word_pair.setAutoDraw(True)
     frameRemains = 1 + 0.8- win.monitorFramePeriod * 0.75  # most of one frame period left
     if word_pair.status == STARTED and t >= frameRemains:
         word_pair.setAutoDraw(False)
     
     # *press_enter* updates
     if t >= 1 and press_enter.status == NOT_STARTED:
         # keep track of start time/frame for later
         press_enter.tStart = t
         press_enter.frameNStart = frameN  # exact frame index
         press_enter.status = STARTED
         # keyboard checking is just starting
         win.callOnFlip(press_enter.clock.reset)  # t=0 on next screen flip
         event.clearEvents(eventType='keyboard')
     frameRemains = 1 + 0.8- win.monitorFramePeriod * 0.75  # most of one frame period left
     if press_enter.status == STARTED and t >= frameRemains:
         press_enter.status = STOPPED
     if press_enter.status == STARTED:
         theseKeys = event.getKeys(keyList=['return'])
         
         # check for quit:
         if "escape" in theseKeys:
             endExpNow = True
         if len(theseKeys) > 0:  # at least one key was pressed
             press_enter.keys = theseKeys[-1]  # just the last key pressed
             press_enter.rt = press_enter.clock.getTime()
             # was this 'correct'?
             if (press_enter.keys == str(correctAns == female_faces and stem_words)) or (press_enter.keys == correctAns == female_faces and stem_words):
                 press_enter.corr = 1
             else:
                 press_enter.corr = 0
             # a response ends the routine
             continueRoutine = False
     
     # *ITI* updates
     if t >= 2.8 and ITI.status == NOT_STARTED:
         # keep track of start time/frame for later
         ITI.tStart = t
         ITI.frameNStart = frameN  # exact frame index
         ITI.setAutoDraw(True)
     frameRemains = 2.8 + 1.0- win.monitorFramePeriod * 0.75  # most of one frame period left
     if ITI.status == STARTED and t >= frameRemains:
         ITI.setAutoDraw(False)
     
       # *happy_face* updates
     if (press_enter.corr == $female_faces and $stem_words) and happy_face.status == NOT_STARTED:
         # keep track of start time/frame for later
         happy_face.tStart = t
         happy_face.frameNStart = frameN  # exact frame index
         happy_face.setAutoDraw(True)
     if happy_face.status == STARTED and t >= (happy_face.tStart + 1.0):
         happy_face.setAutoDraw(False)
     
     # *sad_face* updates
     if () and sad_face.status == NOT_STARTED:
         # keep track of start time/frame for later
         sad_face.tStart = t
         sad_face.frameNStart = frameN  # exact frame index
         sad_face.setAutoDraw(True)
     if sad_face.status == STARTED and t >= (sad_face.tStart + 1.0):
         sad_face.setAutoDraw(False)
     
     # check if all components have finished
     if not continueRoutine:  # a component has requested a forced-end of Routine
         break
     continueRoutine = False  # will revert to True if at least one component still running
     for thisComponent in pr_blComponents:
         if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
             continueRoutine = True
             break  # at least one component has not yet finished
     
     # check for quit (the Esc key)
     if endExpNow or event.getKeys(keyList=["escape"]):
         core.quit()
    

Hope the code and the following condition sheet is helpful to debug my script. act_block.csv (368 Bytes)

Thanks in advance,

Mo

Hi!

I have updated my task and the only issue I am having is to draw the stimuli from the act_block.csv. How to tell psychopy to take the images and word pairs from the act_block.csv to be shown in the trials loop?

I am fighting a deadline so your help is really needed.

Thanks in advance,

Mo

CBT_Updated.psyexp (25.3 KB)
act_block.csv (10.7 KB)
trial_layout.csv (3.8 KB)

@movivi, you could create the entire stim list before the experiment with all conditions satisfied, but it would be randomly generated so that 60 trials pairing female faces with a randomly selected STEM-related word, 20 trials pairing female faces with a randomly selected ARts-related word etc. Would that be good?

Well, if you would like to create the stim list first, which would be a neater way of doing things, without trying to reference different excel sheets, then add a code component to your trial routine, and in the “Begin Experiment” tab:

import pandas as pd

stim = pd.read_csv('act_block.csv') # Get stim

femFaces = list(stim['female_faces'])
maleFaces = list(stim['male_faces'])
stemWords = list(stim['stem_words'])
artWords = list(stim['art_words'])

# Create dict to store face-word pairs
stimDict = {'faceStim': [], 'wordStim': [], 'condition': []}

# stim numbers
stimNumbers = [60, 20, 20, 20]
stimFaces = [femFaces, femFaces, maleFaces, maleFaces]
stimWords = [stemWords, artWords, stemWords, artWords]

for index, block in enumerate(stimNumbers):
    for trials in range(block):
        # randomize order of faces and words
        shuffle(stimFaces[index])
        shuffle(stimWords[index])
        
        stimDict['faceStim'].append(stimFaces[index][0])
        stimDict['wordStim'].append(stimWords[index][0])
        stimDict['condition'].append(index+1)

newStim = pd.DataFrame(stimDict)
newStim.to_csv('newStim.csv', index=False)

This will create a new CSV called “newStim.csv”, which you pass to your trial handler to use for your trials - just make sure your loop is set to random. Using this sheet, all you need to do is use the variables that were created for your stim, e.g., for your image, use $faceStim

Here is an example of newStim.csv

newStim.csv (2.4 KB)

Perfect! Of course this is a better way to run the task. Thank you so much, you’re a lifesaver @dvbridges !

I had a minor issue with providing feedback for these trials. So the idea is that if the participant presses the ENTER key for female face + STEM word, they will be shown a smiley face for 1 second but if they don’t press ENTER key for this type of trials, an X image will be shown to them for 1 second as well.

If they press ENTER key for any other type of trials, an X image will be shown for 1 second and if they don’t respond for those trials, no image will be shown and they can proceed to the next trial after 1 second.

I have tried providing the feedback and now the issue is that it even provides correct feedback for trials where participants have to not respond. How can I change it to fit the criteria I explained above. Your help is so much appreciated!

Following is the code where I think the change needs to be made:

    # *key_resp_2* updates
    if t >= 0 and key_resp_2.status == NOT_STARTED:
        # keep track of start time/frame for later
        key_resp_2.tStart = t
        key_resp_2.frameNStart = frameN  # exact frame index
        key_resp_2.status = STARTED
        # keyboard checking is just starting
        win.callOnFlip(key_resp_2.clock.reset)  # t=0 on next screen flip
        event.clearEvents(eventType='keyboard')
    frameRemains = 0 + 1.8- win.monitorFramePeriod * 0.75  # most of one frame period left
    if key_resp_2.status == STARTED and t >= frameRemains:
        key_resp_2.status = STOPPED
    if key_resp_2.status == STARTED:
        theseKeys = event.getKeys(keyList=['return', 'None'])
        
        # check for quit:
        if "escape" in theseKeys:
            endExpNow = True
        if len(theseKeys) > 0:  # at least one key was pressed
            key_resp_2.keys = theseKeys[-1]  # just the last key pressed
            key_resp_2.rt = key_resp_2.clock.getTime()
            # was this 'correct'?
            if (key_resp_2.keys == str(correctAns)) or (key_resp_2.keys == correctAns):
                key_resp_2.corr = 1
            else:
                key_resp_2.corr = 0
            # a response ends the routine
            continueRoutine = False
    
    # *text_3* updates
    if t >= 0.0 and text_3.status == NOT_STARTED:
        # keep track of start time/frame for later
        text_3.tStart = t
        text_3.frameNStart = frameN  # exact frame index
        text_3.setAutoDraw(True)
    frameRemains = 0.0 + 1.0- win.monitorFramePeriod * 0.75  # most of one frame period left
    if text_3.status == STARTED and t >= frameRemains:
        text_3.setAutoDraw(False)
    
    # check if all components have finished
    if not continueRoutine:  # a component has requested a forced-end of Routine
        break
    continueRoutine = False  # will revert to True if at least one component still running
    for thisComponent in trialComponents:
        if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
            continueRoutine = True
            break  # at least one component has not yet finished
    
    # check for quit (the Esc key)
    if endExpNow or event.getKeys(keyList=["escape"]):
        core.quit()
    
    # refresh the screen
    if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
        win.flip()

# -------Ending Routine "trial"-------
for thisComponent in trialComponents:
    if hasattr(thisComponent, "setAutoDraw"):
        thisComponent.setAutoDraw(False)
# check responses
if key_resp_2.keys in ['', [], None]:  # No response was made
    key_resp_2.keys=None
    # was no response the correct answer?!
    if str(correctAns).lower() == 'none':
       key_resp_2.corr = 1;  # correct non-response
    else:
       key_resp_2.corr = 0;  # failed to respond (incorrectly)
# store data for trials (TrialHandler)
trials.addData('key_resp_2.keys',key_resp_2.keys)
trials.addData('key_resp_2.corr', key_resp_2.corr)
if key_resp_2.keys != None:  # we had a response
    trials.addData('key_resp_2.rt', key_resp_2.rt)

# ------Prepare to start Routine "feedback"-------
t = 0
feedbackClock.reset()  # clock
frameN = -1
continueRoutine = True
routineTimer.add(2.000000)
# update component parameters for each repeat
if key_resp_2.corr == 1:
    happy_face.setImage('o.bmp')
else: happy_face.setImage('x.bmp')
happy_face.setOpacity(1)
happy_face.setPos((0, 0))
happy_face.setSize((0.5, 0.5))
happy_face.setOri(0)
# keep track of which components have finished
feedbackComponents = [happy_face, text_2]
for thisComponent in feedbackComponents:
    if hasattr(thisComponent, 'status'):
        thisComponent.status = NOT_STARTED

@dvbridges I tried running your code but the output it produces is very messy and unlike the sample csv file you provided. Enclosed is the csv file that the code generated on my laptop. I am not sure what’s the issue here.

newStim.csv (5.1 KB)

EDIT: Actually, I had to edit your csv file becaues it had many other entries in the columns that were not relevant. Use the following for your stim. See act_block.csv (260 Bytes)

Still no luck, it gives the same messy csv file.

Thats ok, I edited my post - use the attached stim file above.

Alright, hope you can help me with the aforementioned feedback question as well.

Yes, for the feedback you need a slight amendment to the code I just posted. You want to create a new column for correct answers. You will then use this to determine which feedback to present:

import pandas as pd


stim = pd.read_csv('act_block.csv') # Get stim

femFaces = list(stim['female_faces'])
maleFaces = list(stim['male_faces'])
stemWords = list(stim['stem_words'])
artWords = list(stim['art_words'])

# Create dict to store face-word pairs
stimDict = {'faceStim': [], 'wordStim': [], 'condition': [], 'correctAns': []}

# stim numbers
stimNumbers = [60, 20, 20, 20]
stimFaces = [femFaces, femFaces, maleFaces, maleFaces]
stimWords = [stemWords, artWords, stemWords, artWords]

for index, block in enumerate(stimNumbers):
    for trials in range(block):
        # randomize order of faces and words
        shuffle(stimFaces[index])
        shuffle(stimWords[index])
        
        stimDict['faceStim'].append(stimFaces[index][0])
        stimDict['wordStim'].append(stimWords[index][0])
        stimDict['condition'].append(index+1)
        if index == 0:  # Only use 'return' as correctAns for femFaces with stemWords
            stimDict['correctAns'].append('return')
        else:
            stimDict['correctAns'].append('None')

newStim = pd.DataFrame(stimDict)
newStim.to_csv('newStim.csv', index=False)

So you will want to use the correctAns variable in your keyboard, which I beleive you have already done.

@dvbridges well the issue then is that I need to generate the csv file on my laptop with that additional column showing the correct response criteria and the enclosed csv is again showing a messy output. It would be ideal if I could produce similar csv as the sample you uploaded earlier. Also it might be important for me to show my supervisor how the pseudorandom sequence is generated on my own laptop.

I am not sure why the code is shuffling across rows and putting values of a specific column into another column as well as why the female and male pictures are all grouped together in one single cell.

newStim.csv (5.6 KB)

If you look at your act_block.csv sheet, scroll down the sheet. Keep scrolling until you see entries that should not be there. You need to delete them.

Your version

My amended version - with entries at row 41 onwards deleted

1 Like

Regarding your feedback, try the following in your feedback routine. In the code component, in the “Begin Routine” tab:

# Correct ('return') response to female face + stem
if key_resp_2.corr and correctAns == 'return':  
    message = 'Smily face'
# Incorrect ('None') response to female face + stem
elif not key_resp_2.corr and correctAns == 'return':
    message = 'X'
# Correct ('None') response to any face + any word except above
elif key_resp_2.corr and correctAns == 'None':
    message = ''
# Inorrect ('return') response to any face + any word except above
elif not key_resp_2.corr and correctAns == 'None':
    message = 'X'

Actually I tried doing something similar earlier and still the feedback was not right. Using your code: the incorrect feedback is given even for those trials I don’t need to respond. Following is the snippet from the script:

# Correct ('return') response to female face + stem
if key_resp_2.corr and correctAns == 'return':  
    happy_face.setImage('o.bmp')
    # Incorrect ('None') response to female face + stem
elif not key_resp_2.corr and correctAns == 'return':
    happy_face.setImage('x.bmp')
    # Correct ('None') response to any face + any word except above
elif key_resp_2.corr and correctAns == 'None':
    happy_face.setAutoDraw(False)
# Inorrect ('return') response to any face + any word except above
elif not key_resp_2.corr and correctAns == 'None':
    happy_face.setImage('x.bmp')

I am displaying pictures as feedback so o.bmp is the smiley face and the x.bmp is the X letter.

Have you tried it with the new code? It seems to work for me:

femFace + STEM + return = smily
femFace + STEM + None = X
femFace + ART + return = X
femFace + ART + None = Blank

With males, any response to any word image pair will give a ‘X’.

Yes I tried it with your code too and the code I pasted above is actually yours where I have replaced the message feedback to image feedback. Is there a different way to show image as a feedback?

Do you have an image component in your feedback trial called “happy_face”?