Search code examples
pythonpsychopy

Variable Trial Conditions in PsychoPy


I am using PsychoPy version 1.82.01 in the Builder View. My knowledge of Python is still very limited. I've run into a roadblock with a task and would appreciate any advice from the community!

The task goes like this:

Subject is first presented with two target triangles: one pointing to the left, one pointing to the right. The subject is instructed to press the corresponding left or right arrow keys when they spot either one of these targets.

A sample of the Excel conditions file looks like this:

angle_01    angle_02    angle_03    angle_04    corrAns
   0           45          135        180        none
  315          45          135        180        none
  315         225          135        180        none
  270         225          135        180        left
   0          225          135        180        none

**The subject is then presented with an array of 4 triangles. I created these 4 triangles in Builder. Each triangle is set to have a 1 second duration. The orientation of each triangle is determined by the conditions file--triangle 1 orientation field is set to "angle_01," triangle 2 orientation is set to "angle_02" and so forth. So, if you can see in the snippet above, I created each row in the conditions file such that only one triangle changes its orientation every second. When one of the triangle changes like this, it is called a "distractor shift."

After a number of these distractor shifts, one of the triangles shifts into the "target" position (either pointing left or right, 90 or 270 degrees), at which point the person is meant to respond with a keypress. This is the "target shift." I have the orientations for all triangles specified in an Excel conditions file. Right now I have this set as a sequential loop, with fixed numbers of distractor shifts, for the sake of simplicity.

However, my ultimate goal is to have a variable number of distractor shifts before the target shift in each trial, and for each trial to end with a fixation cross before moving onto the next trial. I am having a hard time figuring out what code to use to make this possible. I started to make the loop proceed through the conditions file randomly, but the result is that every triangle changes its orientation at every rep (rather than just one at a time). Have I gone about this in the wrong way? The full code pasted below.

Thank you!

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
This experiment was created using PsychoPy2 Experiment Builder (v1.82.01), Thu Aug 27 10:56:53 2015
If you publish work using this script please cite the relevant PsychoPy publications
  Peirce, JW (2007) PsychoPy - Psychophysics software in Python. Journal of Neuroscience Methods, 162(1-2), 8-13.
  Peirce, JW (2009) Generating stimuli for neuroscience using PsychoPy. Frontiers in Neuroinformatics, 2:10. doi: 10.3389/neuro.11.010.2008
"""

from __future__ import division  # so that 1/3=0.333 instead of 1/3=0
from psychopy import visual, core, data, event, logging, sound, gui
from psychopy.constants import *  # things like STARTED, FINISHED
import numpy as np  # whole numpy lib is available, prepend 'np.'
from numpy import sin, cos, tan, log, log10, pi, average, sqrt, std, deg2rad, rad2deg, linspace, asarray
from numpy.random import random, randint, normal, shuffle
import os  # handy system and path functions

# Ensure that relative paths start from the same directory as this script
_thisDir = os.path.dirname(os.path.abspath(__file__))
os.chdir(_thisDir)

# Store info about the experiment session
expName = 'simultaneous_04'  # from the Builder filename that created this script
expInfo = {u'session': u'001', u'participant': u''}
dlg = gui.DlgFromDict(dictionary=expInfo, title=expName)
if dlg.OK == False: core.quit()  # user pressed cancel
expInfo['date'] = data.getDateStr()  # add a simple timestamp
expInfo['expName'] = expName

# Data file name stem = absolute path + name; later add .psyexp, .csv, .log, etc
filename = _thisDir + os.sep + 'data/%s_%s_%s' %(expInfo['participant'], expName, expInfo['date'])

# An ExperimentHandler isn't essential but helps with data saving
thisExp = data.ExperimentHandler(name=expName, version='',
    extraInfo=expInfo, runtimeInfo=None,
    originPath=None,
    savePickle=True, saveWideText=True,
    dataFileName=filename)
#save a log file for detail verbose info
logFile = logging.LogFile(filename+'.log', level=logging.EXP)
logging.console.setLevel(logging.WARNING)  # this outputs to the screen, not a file

endExpNow = False  # flag for 'escape' or other condition => quit the exp

# Start Code - component code to be run before the window creation

# Setup the Window
win = visual.Window(size=(1366, 768), fullscr=True, screen=0, allowGUI=False, allowStencil=False,
    monitor='testMonitor', color=[-1,-1,-1], colorSpace='rgb',
    blendMode='avg', useFBO=True,
    )
# store frame rate of monitor if we can measure it successfully
expInfo['frameRate']=win.getActualFrameRate()
if expInfo['frameRate']!=None:
    frameDur = 1.0/round(expInfo['frameRate'])
else:
    frameDur = 1.0/60.0 # couldn't get a reliable measure so guess

# Initialize components for Routine "Target_Presentation"
Target_PresentationClock = core.Clock()
target_triangle = visual.ShapeStim(win=win, name='target_triangle',units='cm', 
    vertices = [[-[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [+[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [0,[5.5,5.89][1]/2.0]],
    ori=1.0, pos=[-7, 3],
    lineWidth=1, lineColor=[1,1,1], lineColorSpace='rgb',
    fillColor=[1,1,1], fillColorSpace='rgb',
    opacity=1,depth=0.0, 
interpolate=True)
text_4 = visual.TextStim(win=win, ori=0, name='text_4',
    text='Press the LEFT ARROW key when you spot the triangle pointing to the left.\n\nPress the RIGHT ARROW key when you spot the triangle pointing to the right.\n\nPress the spacebar when you are ready to begin.',    font='Arial',
    pos=[0,-0.5], height=0.08, wrapWidth=None,
    color='white', colorSpace='rgb', opacity=1,
    depth=-1.0)
target_triangle_02 = visual.ShapeStim(win=win, name='target_triangle_02',units='cm', 
    vertices = [[-[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [+[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [0,[5.5,5.89][1]/2.0]],
    ori=90, pos=[7,3],
    lineWidth=1, lineColor=[1,1,1], lineColorSpace='rgb',
    fillColor=[1,1,1], fillColorSpace='rgb',
    opacity=1,depth=-2.0, 
interpolate=True)
text_5 = visual.TextStim(win=win, ori=0, name='text_5',
    text='or',    font='Arial',
    pos=[0, 0.25], height=0.1, wrapWidth=None,
    color='white', colorSpace='rgb', opacity=1,
    depth=-3.0)

# Initialize components for Routine "Start_Fixation_Cross"
Start_Fixation_CrossClock = core.Clock()
text_2 = visual.TextStim(win=win, ori=0, name='text_2',
    text='+',    font='Arial',
    pos=[0, 0], height=0.1, wrapWidth=None,
    color='white', colorSpace='rgb', opacity=1,
    depth=0.0)

# Initialize components for Routine "Trial"
TrialClock = core.Clock()


triangle_01 = visual.ShapeStim(win=win, name='triangle_01',units='cm', 
    vertices = [[-[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [+[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [0,[5.5,5.89][1]/2.0]],
    ori=1.0, pos=[-8.2,6.2],
    lineWidth=1, lineColor=[1,1,1], lineColorSpace='rgb',
    fillColor=[1,1,1], fillColorSpace='rgb',
    opacity=1,depth=-1.0, 
interpolate=True)
triangle_02 = visual.ShapeStim(win=win, name='triangle_02',units='cm', 
    vertices = [[-[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [+[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [0,[5.5,5.89][1]/2.0]],
    ori=1.0, pos=[8.2,6.2],
    lineWidth=1, lineColor=[1,1,1], lineColorSpace='rgb',
    fillColor=[1,1,1], fillColorSpace='rgb',
    opacity=1,depth=-2.0, 
interpolate=True)
triangle_03 = visual.ShapeStim(win=win, name='triangle_03',units='cm', 
    vertices = [[-[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [+[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [0,[5.5,5.89][1]/2.0]],
    ori=1.0, pos=[8.2,-6.2],
    lineWidth=1, lineColor=[1,1,1], lineColorSpace='rgb',
    fillColor=[1,1,1], fillColorSpace='rgb',
    opacity=1,depth=-3.0, 
interpolate=True)
triangle_04 = visual.ShapeStim(win=win, name='triangle_04',units='cm', 
    vertices = [[-[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [+[5.5,5.89][0]/2.0,-[5.5,5.89][1]/2.0], [0,[5.5,5.89][1]/2.0]],
    ori=1.0, pos=[-8.2,-6.2],
    lineWidth=1, lineColor=[1,1,1], lineColorSpace='rgb',
    fillColor=[1,1,1], fillColorSpace='rgb',
    opacity=1,depth=-4.0, 
interpolate=True)

# Initialize components for Routine "Ending"
EndingClock = core.Clock()
text_3 = visual.TextStim(win=win, ori=0, name='text_3',
    text='This concludes the experiment. \n\nThank you for participating!',    font='Arial',
    pos=[0, 0], height=0.1, wrapWidth=None,
    color='white', colorSpace='rgb', opacity=1,
    depth=0.0)

# Create some handy timers
globalClock = core.Clock()  # to track the time since experiment started
routineTimer = core.CountdownTimer()  # to track time remaining of each (non-slip) routine 

#------Prepare to start Routine "Target_Presentation"-------
t = 0
Target_PresentationClock.reset()  # clock 
frameN = -1
# update component parameters for each repeat
target_triangle.setOri(270)
key_resp_3 = event.BuilderKeyResponse()  # create an object of type KeyResponse
key_resp_3.status = NOT_STARTED
# keep track of which components have finished
Target_PresentationComponents = []
Target_PresentationComponents.append(target_triangle)
Target_PresentationComponents.append(text_4)
Target_PresentationComponents.append(target_triangle_02)
Target_PresentationComponents.append(text_5)
Target_PresentationComponents.append(key_resp_3)
for thisComponent in Target_PresentationComponents:
    if hasattr(thisComponent, 'status'):
        thisComponent.status = NOT_STARTED

#-------Start Routine "Target_Presentation"-------
continueRoutine = True
while continueRoutine:
    # get current time
    t = Target_PresentationClock.getTime()
    frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
    # update/draw components on each frame

    # *target_triangle* updates
    if t >= 0.0 and target_triangle.status == NOT_STARTED:
        # keep track of start time/frame for later
        target_triangle.tStart = t  # underestimates by a little under one frame
        target_triangle.frameNStart = frameN  # exact frame index
        target_triangle.setAutoDraw(True)

    # *text_4* updates
    if t >= 0.0 and text_4.status == NOT_STARTED:
        # keep track of start time/frame for later
        text_4.tStart = t  # underestimates by a little under one frame
        text_4.frameNStart = frameN  # exact frame index
        text_4.setAutoDraw(True)

    # *target_triangle_02* updates
    if t >= 0.0 and target_triangle_02.status == NOT_STARTED:
        # keep track of start time/frame for later
        target_triangle_02.tStart = t  # underestimates by a little under one frame
        target_triangle_02.frameNStart = frameN  # exact frame index
        target_triangle_02.setAutoDraw(True)

    # *text_5* updates
    if t >= 0.0 and text_5.status == NOT_STARTED:
        # keep track of start time/frame for later
        text_5.tStart = t  # underestimates by a little under one frame
        text_5.frameNStart = frameN  # exact frame index
        text_5.setAutoDraw(True)

    # *key_resp_3* updates
    if t >= 0.0 and key_resp_3.status == NOT_STARTED:
        # keep track of start time/frame for later
        key_resp_3.tStart = t  # underestimates by a little under one frame
        key_resp_3.frameNStart = frameN  # exact frame index
        key_resp_3.status = STARTED
        # keyboard checking is just starting
        key_resp_3.clock.reset()  # now t=0
        event.clearEvents(eventType='keyboard')
    if key_resp_3.status == STARTED:
        theseKeys = event.getKeys(keyList=['space'])

        # check for quit:
        if "escape" in theseKeys:
            endExpNow = True
        if len(theseKeys) > 0:  # at least one key was pressed
            key_resp_3.keys = theseKeys[-1]  # just the last key pressed
            key_resp_3.rt = key_resp_3.clock.getTime()
            # a response ends the routine
            continueRoutine = 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 Target_PresentationComponents:
        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 "Target_Presentation"-------
for thisComponent in Target_PresentationComponents:
    if hasattr(thisComponent, "setAutoDraw"):
        thisComponent.setAutoDraw(False)
# check responses
if key_resp_3.keys in ['', [], None]:  # No response was made
   key_resp_3.keys=None
# store data for thisExp (ExperimentHandler)
thisExp.addData('key_resp_3.keys',key_resp_3.keys)
if key_resp_3.keys != None:  # we had a response
    thisExp.addData('key_resp_3.rt', key_resp_3.rt)
thisExp.nextEntry()
# the Routine "Target_Presentation" was not non-slip safe, so reset the non-slip timer
routineTimer.reset()

#------Prepare to start Routine "Start_Fixation_Cross"-------
t = 0
Start_Fixation_CrossClock.reset()  # clock 
frameN = -1
routineTimer.add(1.000000)
# update component parameters for each repeat
# keep track of which components have finished
Start_Fixation_CrossComponents = []
Start_Fixation_CrossComponents.append(text_2)
for thisComponent in Start_Fixation_CrossComponents:
    if hasattr(thisComponent, 'status'):
        thisComponent.status = NOT_STARTED

#-------Start Routine "Start_Fixation_Cross"-------
continueRoutine = True
while continueRoutine and routineTimer.getTime() > 0:
    # get current time
    t = Start_Fixation_CrossClock.getTime()
    frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
    # update/draw components on each frame

    # *text_2* updates
    if t >= 0.0 and text_2.status == NOT_STARTED:
        # keep track of start time/frame for later
        text_2.tStart = t  # underestimates by a little under one frame
        text_2.frameNStart = frameN  # exact frame index
        text_2.setAutoDraw(True)
    if text_2.status == STARTED and t >= (0.0 + (1.0-win.monitorFramePeriod*0.75)): #most of one frame period left
        text_2.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 Start_Fixation_CrossComponents:
        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 "Start_Fixation_Cross"-------
for thisComponent in Start_Fixation_CrossComponents:
    if hasattr(thisComponent, "setAutoDraw"):
        thisComponent.setAutoDraw(False)

# set up handler to look after randomisation of conditions etc
TrialLoop = data.TrialHandler(nReps=1, method='sequential', 
    extraInfo=expInfo, originPath=None,
    trialList=data.importConditions('distractor_conditions.xlsx'),
    seed=None, name='TrialLoop')
thisExp.addLoop(TrialLoop)  # add the loop to the experiment
thisTrialLoop = TrialLoop.trialList[0]  # so we can initialise stimuli with some values
# abbreviate parameter names if possible (e.g. rgb=thisTrialLoop.rgb)
if thisTrialLoop != None:
    for paramName in thisTrialLoop.keys():
        exec(paramName + '= thisTrialLoop.' + paramName)

for thisTrialLoop in TrialLoop:
    currentLoop = TrialLoop
    # abbreviate parameter names if possible (e.g. rgb = thisTrialLoop.rgb)
    if thisTrialLoop != None:
        for paramName in thisTrialLoop.keys():
            exec(paramName + '= thisTrialLoop.' + paramName)

    #------Prepare to start Routine "Trial"-------
    t = 0
    TrialClock.reset()  # clock 
    frameN = -1
    routineTimer.add(1.000000)
    # update component parameters for each repeat



    triangle_01.setOri(distractor_angle_01)
    triangle_02.setOri(distractor_angle_02)
    triangle_03.setOri(distractor_angle_03)
    triangle_04.setOri(distractor_angle_04)
    trial_resp = event.BuilderKeyResponse()  # create an object of type KeyResponse
    trial_resp.status = NOT_STARTED
    # keep track of which components have finished
    TrialComponents = []
    TrialComponents.append(triangle_01)
    TrialComponents.append(triangle_02)
    TrialComponents.append(triangle_03)
    TrialComponents.append(triangle_04)
    TrialComponents.append(trial_resp)
    for thisComponent in TrialComponents:
        if hasattr(thisComponent, 'status'):
            thisComponent.status = NOT_STARTED

    #-------Start Routine "Trial"-------
    continueRoutine = True
    while continueRoutine and routineTimer.getTime() > 0:
        # get current time
        t = TrialClock.getTime()
        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
        # update/draw components on each frame




        # *triangle_01* updates
        if t >= 0 and triangle_01.status == NOT_STARTED:
            # keep track of start time/frame for later
            triangle_01.tStart = t  # underestimates by a little under one frame
            triangle_01.frameNStart = frameN  # exact frame index
            triangle_01.setAutoDraw(True)
        if triangle_01.status == STARTED and t >= (0 + (1.0-win.monitorFramePeriod*0.75)): #most of one frame period left
            triangle_01.setAutoDraw(False)

        # *triangle_02* updates
        if t >= 0 and triangle_02.status == NOT_STARTED:
            # keep track of start time/frame for later
            triangle_02.tStart = t  # underestimates by a little under one frame
            triangle_02.frameNStart = frameN  # exact frame index
            triangle_02.setAutoDraw(True)
        if triangle_02.status == STARTED and t >= (0 + (1.0-win.monitorFramePeriod*0.75)): #most of one frame period left
            triangle_02.setAutoDraw(False)

        # *triangle_03* updates
        if t >= 0 and triangle_03.status == NOT_STARTED:
            # keep track of start time/frame for later
            triangle_03.tStart = t  # underestimates by a little under one frame
            triangle_03.frameNStart = frameN  # exact frame index
            triangle_03.setAutoDraw(True)
        if triangle_03.status == STARTED and t >= (0 + (1.0-win.monitorFramePeriod*0.75)): #most of one frame period left
            triangle_03.setAutoDraw(False)

        # *triangle_04* updates
        if t >= 0 and triangle_04.status == NOT_STARTED:
            # keep track of start time/frame for later
            triangle_04.tStart = t  # underestimates by a little under one frame
            triangle_04.frameNStart = frameN  # exact frame index
            triangle_04.setAutoDraw(True)
        if triangle_04.status == STARTED and t >= (0 + (1.0-win.monitorFramePeriod*0.75)): #most of one frame period left
            triangle_04.setAutoDraw(False)

        # *trial_resp* updates
        if t >= 0.0 and trial_resp.status == NOT_STARTED:
            # keep track of start time/frame for later
            trial_resp.tStart = t  # underestimates by a little under one frame
            trial_resp.frameNStart = frameN  # exact frame index
            trial_resp.status = STARTED
            # keyboard checking is just starting
            trial_resp.clock.reset()  # now t=0
            event.clearEvents(eventType='keyboard')
        if trial_resp.status == STARTED and t >= (0.0 + (1.0-win.monitorFramePeriod*0.75)): #most of one frame period left
            trial_resp.status = STOPPED
        if trial_resp.status == STARTED:
            theseKeys = event.getKeys(keyList=['left', 'right'])

            # check for quit:
            if "escape" in theseKeys:
                endExpNow = True
            if len(theseKeys) > 0:  # at least one key was pressed
                trial_resp.keys = theseKeys[-1]  # just the last key pressed
                trial_resp.rt = trial_resp.clock.getTime()
                # was this 'correct'?
                if (trial_resp.keys == str(corrAns)) or (trial_resp.keys == corrAns):
                    trial_resp.corr = 1
                else:
                    trial_resp.corr = 0

        # 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 trial_resp.keys in ['', [], None]:  # No response was made
       trial_resp.keys=None
       # was no response the correct answer?!
       if str(corrAns).lower() == 'none': trial_resp.corr = 1  # correct non-response
       else: trial_resp.corr = 0  # failed to respond (incorrectly)
    # store data for TrialLoop (TrialHandler)
    TrialLoop.addData('trial_resp.keys',trial_resp.keys)
    TrialLoop.addData('trial_resp.corr', trial_resp.corr)
    if trial_resp.keys != None:  # we had a response
        TrialLoop.addData('trial_resp.rt', trial_resp.rt)
    thisExp.nextEntry()

# completed 1 repeats of 'TrialLoop'


#------Prepare to start Routine "Ending"-------
t = 0
EndingClock.reset()  # clock 
frameN = -1
routineTimer.add(3.000000)
# update component parameters for each repeat
# keep track of which components have finished
EndingComponents = []
EndingComponents.append(text_3)
for thisComponent in EndingComponents:
    if hasattr(thisComponent, 'status'):
        thisComponent.status = NOT_STARTED

#-------Start Routine "Ending"-------
continueRoutine = True
while continueRoutine and routineTimer.getTime() > 0:
    # get current time
    t = EndingClock.getTime()
    frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
    # update/draw components on each frame

    # *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  # underestimates by a little under one frame
        text_3.frameNStart = frameN  # exact frame index
        text_3.setAutoDraw(True)
    if text_3.status == STARTED and t >= (0.0 + (3.0-win.monitorFramePeriod*0.75)): #most of one frame period left
        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 EndingComponents:
        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 "Ending"-------
for thisComponent in EndingComponents:
    if hasattr(thisComponent, "setAutoDraw"):
        thisComponent.setAutoDraw(False)

win.close()
core.quit()

Solution

  • The layout of your Excel conditions file will probably need to change somewhat. PsychoPy is structured around treating each row in that file as corresponding to a trial, whereas you are currently spreading a trial across multiple rows. That will make it harder to store and collect responses appropriately with their corresponding stimuli. Suggest you lay it out like this:

    numShifts     angle_01         angle_02
    4             [0,315,315,270]  [45,45,225,225]
    3             [0,45,315]        [45,45,90]
    etc
    

    Then in Builder, insert a new loop, inside your existing one.

    Importantly: de-select its "is trials" check box. That means this loop will run multiple times within a single trial, shifting the stimuli however many times are required for this particular trial.

    Let's call the inner one stimulusLoop and the outer trial-level loop trialLoop). i.e. trialLoop will run just once per trial, processing one line of the conditions file. stimulusLoop will extract each entry of the list of angles for each stimulus, WITHIN a trial. To achieve this, put numShift in the nReps field of the inner loop. e.g. given the conditions file above, it would run 4 times on the first trial, and 3 times on the second.

    Then in the orientation field of each stimulus, put something like this (untested):

    eval(angle_01)[stimulusLoop.thisN]

    What this does is evaluate the list of angles (when it is read in, PsychoPy just thinks it is the list of characters "[0,315,315,270]". The eval() function tells it to evaluate it as a Python expression. In this case, it will realise it is a list of numbers and we can then index it to get the current value, in this case using the current iteration number of the inner loop (stimulusLoop.thisN).

    Hopefully this will get you started.