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.
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'}