Python script for trial randomization

In this post you will find a Python script for randomizing conditions with constraints (i.e., never 2 consecutive trials of X). This script will in a later post be implemented in a cross-modal oddball task created in Psychopy.

I recently started to use Psychopy to build experiments. To build my experiments (mainly oddball tasks) I have, up until I found psychopy, used e-prime. In an oddball task one does not usually present several oddball (typically called “novel” or “deviant”) stimuli in succession. Without getting into detail; I was not able to solve this problem with e-primes scripting language “e-basic” so I made a python script instead. With this script I pre-generated .txt-files that I loaded into e-prime and got my constraints fulfilled.

PsychoPy is based on Python and it felt natural (I know Python better than e-basic), therefore, to program my new experiment in Psychopy and implement my script there instead. That is, the code will randomize trials on the fly for each participant.

The following script is intended to use with TrialHandler in Psychopy (trials = data.TrialHandler (listcreatedbyscript, 1, method = “sequential”). Sequential method will be used because the script already randomizes trials. A further feature of the script is that every oddball is presented an equal number of times for each digit (3 times for each digit in each block). However, it is possible that I will skip using the TrialHandler.

Update: In a new post I have created another randomization script. In the new script I do not use PsychoPy’s  TrialHandler. That is, it is not dependent on using PsychoPy.

Click here for link to the post.

Python script:

#/usr/bin/env python
#Pseudo-randomization python script for a cross-modal oddball task
#erik marsja, https://www.marsja.se, erik at marsja.se
#Script made to use with Psychopy

#Need to import random and regular expression
import random, re
#Function for applying standard and deviants to a list. In this list no randomization is done.
def expSetup(subid, rkeys, digits, block):
    dlist = []
    templist = []
    trialN=0
    for i in range(len(digits)):
        tmp = 0
        for b in range(1,16): #For 120 trials make the number of digits even
            trialN +=1
            if tmp <= 2:#
                cond="deviant"
                tmp += 1
            else: cond="standard"
            #Setting correct responses and everything else. Each trial is one dict in the list
            if i%2==0:
                templist.append({'Sub_id':subid,'Trial':0, 'Block':block+1, 'Condition':cond,'Corr':rkeys['Even'],'Target':digits[i]})
            else: 
                templist.append({'Sub_id':subid,'Trial':0, 'Block':block+1, 'Condition':cond,'Corr':rkeys['Odd'],'Target':digits[i]})
        dlist = templist
    return dlist

def swap(a, i, j):
    '''swap items i and j in the list a'''
    a[i], a[j] = a[j], a[i]
def moveNovel(ind, nT, trials):
    for a in range(nT):
        s = random.randint(0,nT-2)
        try:    
                prev1 = trials[s-1]["Condition"]
                prev2 = trials[s-2]["Condition"]
                nex1 =  trials[s+1]["Condition"]
                nex2 = trials[s+2]["Condition"]
                '''Checks if index is surrounded by "Standards"...
                Move the novel if that is the case. Added the last and () to have 2 standards'''
                if mObS.match(trials[s]["Condition"]) and ((mObS.match(prev1)and mObS.match(nex1)) \
                and (mObS.match(prev2)and mObS.match(nex2))):
                    c = s
                    swap(trials, ind, c)
        except IndexError:
            continue
   
def randomizeStim(trials, nT):#trials = list of trials dicts.. nT = number of trials
#Randomize the list
#Trials = the list of trials made with expSetup. A list of dicts...
    random.shuffle(trials)
#make sure there are not two consequitive deviants
    for i in range(nT):
        if mObD.match(trials[i]["Condition"]) and (i == 0 or i == 119):
            b = i
            moveNovel(b, nT, trials)
        try:#Checking indexes surrounding trial i
            prev = trials[i-1]["Condition"]
            prev2 = trials[i-2]["Condition"]
            nex1 = trials[i+1]["Condition"]
            nex2 = trials[i+2]["Condition"]
            if mObD.match(trials[i]["Condition"]) and ((mObD.match(prev) or mObD.match(nex1)) \
            or (mObD.match(prev) and mObD.match(nex1)) or (mObD.match(prev2) or mObD.match(nex2))):
                b = i
                moveNovel(b, nT, trials)
            #Added to have at least 2 standards between devs
        except IndexError:
            continue
        '''Not the first and the last in each block is a novel. Might not be a problem after all...'''            
    return trials

"Regexp for matching different novels"
mObD = re.compile(r'deviant\W*\d*')
mObS = re.compile(r'standard\W*\d*')
def trialCreator(parN, nTrials, blocks):
    '''Starting with counterbalencing the response buttons
    even participant numbers get 'x' as odd and 'z' as even'''
    if parN%2==0: rkeys = {'Odd':'x', 'Even':'z'}
    else: rkeys = {'Odd':'z', 'Even':'x'}
    #creates list of digits to be used
    digits=[]
    for i in range(1,9):
        digits.append(str(i))
    subid=parN
    bTrials = nTrials/blocks #How many trials/block?
    templ = []
    for i in range(blocks):
        digList=expSetup(subid, rkeys, digits, i)
        stims = randomizeStim(digList, bTrials)
        for trialN in range(len(stims)):
            stims[trialN]['Trial'] = trialN+1
        templ = templ + stims
    return templ
if __name__ == "__main__":
    tr = trialCreator(parN=1, nTrials = 720, blocks= 6) #Gets a list of trials for all blocks

The Python script will create a list containing dictionaries. Each dictionary is one Trial and contains the correct button, condition, visual target, block and trial #.
Below are the 10 first dictionaries printed (first block, first 10 trials):

## Example for the first 10 trials (block 1):
##
## {'Target': '6', 'Sub_id': 1, 'Trial': 1, 'Block': 1, 'Corr': 'z', 'Condition': 'standard'}
##
## {'Target': '5', 'Sub_id': 1, 'Trial': 2, 'Block': 1, 'Corr': 'x', 'Condition': 'standard'}
##
## {'Target': '4', 'Sub_id': 1, 'Trial': 3, 'Block': 1, 'Corr': 'z', 'Condition': 'deviant'}
##
## {'Target': '3', 'Sub_id': 1, 'Trial': 4, 'Block': 1, 'Corr': 'x', 'Condition': 'standard'}
##
## {'Target': '6', 'Sub_id': 1, 'Trial': 5, 'Block': 1, 'Corr': 'z', 'Condition': 'standard'}
##
## {'Target': '5', 'Sub_id': 1, 'Trial': 6, 'Block': 1, 'Corr': 'x', 'Condition': 'deviant'}
##
## {'Target': '5', 'Sub_id': 1, 'Trial': 7, 'Block': 1, 'Corr': 'x', 'Condition': 'standard'}
##
## {'Target': '2', 'Sub_id': 1, 'Trial': 8, 'Block': 1, 'Corr': 'z', 'Condition': 'standard'}
##
## {'Target': '4', 'Sub_id': 1, 'Trial': 9, 'Block': 1, 'Corr': 'z', 'Condition': 'standard'}
##
## {'Target': '2', 'Sub_id': 1, 'Trial': 10, 'Block': 1, 'Corr': 'z', 'Condition': 'deviant'}

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to Top
Share via
Copy link
Powered by Social Snap