How to create stimuli that consist of multiple (i.e., hundreds) of rectangles

Win10:
PsychoPy version 3:

I’m new to PsychoPy/python- does anyone know if there is a way to create a stimuli that consists of 100 rectangles/lines? I want them to be randomly positioned around the centre of the screen with orientations sampled from a gaussian distribution. I can make all of the above happen with just one rectangle, but when I try to draw multiple rectangles I run into trouble (i.e., I continue to get one rectangle or I get an error message). I’ve tried using a for loop in the builder’s coding window:

mu = 135
sigma = 15 
line_x = np.random.uniform(-0.2,0.2, 100)

for i in range(100):
    
    pos = [line_x[i],np.random.uniform(-0.2,0.2)]
    ori = random.gauss(mu, sigma)

But this just returns one rectangle that’s randomly positioned and has a random orientation with a mean of 135 degrees and an SD of 15 degrees.

If anyone has any ideas, I’d be very grateful to hear them- thank you.

I think what you’re after is a DotStim :dots: but with rectangles rather than circles, here’s the documentation for DotStim:
https://www.psychopy.org/api/visual/dotstim.html

What you’ll need to do is make a Polygon :polygon: whose shape, size, colour, etc. are what you want each dot to be and then supply that to DotStim as the parameter “element”

Thanks for your insight.
I’ve had a go at using DotStim, setting up a rectangle and then setting dots.element to call that rectangle:

dots.element = rect

for i in range(100):
    
    ori=(random.gauss(mu, sigma))

So, I’ve removed all the positional information from the code and replaced it with dots.element = rect. But now I’m not getting any rectangles (or dots), just a blank screen and I’m not getting an error message so I’m not sure where I’m going wrong. I thought it might be that the units were off (as the documentations says that dotStim.element assumes pix), but I’ve checked and all should be well. I’d be thankful for any thoughts you might have.

My apologies, there was a typo elsewhere in my code; all fixed and working now. Thank you very much for your help.

UPDATE: after playing around with the values, I’ve noticed that something is not quite right- I’ve managed to draw 100 rectangles at the same time, but they are all oriented the same direction (i.e., it’s taking one rectangle and repeating; so not a gaussian distribution). I’ve tried running script from a previous experiment that used dotStim to sample a gaussian distribution successfully:

nums = []
# for trials with stimuli pointing to the right, mu=45, and for left, mu=135
mu = 135
# sigma is the value we'll change.
sigma = 25

# generate an array  of 100 values (type int) and append to var 'nums'
for i in range(100):  
    gauss = random.gauss(mu, sigma)  
    nums.append(gauss)

However, when I run this code one of two things happens; if I call ‘nums’ as a value for the dot’s directions (i.e., direction = $nums) nothing happens (I’m assuming because the one rectangle is being repeated 100 times by dotStim). If I call ‘nums’ as a value for the rectangle’s orientations (i.e., orientation = $nums), I get an error message:

ValueError: operands could not be broadcast together with shapes (2,) (2,2,100)

I’ve reduced *range*(100) to *range*(1) and that resolves the error and the experiment works, but the rectangles are still all oriented the same way (i.e., it’s just one rectangle repeated 100 times).

Any further suggestions would be greatly appreciated- thanks.

1 Like

I think the issue here is that the dir attribute of a DotStim is the direction of motion of the elements, not their orientation. As the DotStim is built to display, well, dots, orientation isn’t something it provides for.

What you probably want here is the ElementArrayStim, which would allow you to quickly draw 100 copies of a single rectangle, but each can have its own independent attributes (such as orientation).

https://www.psychopy.org/api/visual/elementarraystim.html#psychopy.visual.ElementArrayStim

The easiest way to specify the texture to draw (i.e. your rectangle) would just be to pass a bitmap image of rectangle as the tex parameter. It might be possible to do that by specifying vertices too, but I’m not sure how.

PS when you’re discussing problems with code, please give the actual code verbatim. e.g. in narrative form, a statement like “if I call ‘nums’ as a value for the dot’s directions (i.e., direction = $nums) nothing happens” is not very useful. For a start, the \$ symbol is not valid Python syntax and should have caused an explicit error, and directions is just the name of a variable, not an attribute of a stimulus, so we will assume you’re actually doing something other than what you describe, and won’t be able to help much.

Lastly, the ElementArrayStim is optimised to very quickly draw many stimuli. You don’t want to slow that down by taking more time than necessary to feed it the numbers it needs to control positions and orientations and so on. So where ever possible, avoid slow Python loops like this:

for i in range(100):  
    gauss = random.gauss(mu, sigma)  
    nums.append(gauss)

and don’t use the random module from the Python standard library. Instead (like all PsychoPy Builder scripts), use numpy's highly optimised vectorised functions, to do it in one step like this:

nums = np.random.normal(loc = 135, scale = 25, size = 100)

Builder scripts all import the numpy library and make it available to you with the abbreviation np, as it is used so commonly.
https://numpy.org/doc/stable/reference/random/generated/numpy.random.normal.html

Here’s an example of rectangular dots that work online.

for Idx in range (maxNoise):
    thisColour=randint(0,33)+randint(0,33)+randint(0,33)+randint(0,33)
    noise.append(visual.Rect(
    win=win,
    name='noise'+str(Idx),
       fillColor=grey[thisColour],
        lineColor=grey[thisColour],
    pos = [(random()-.5)/2,(random()-.5)/2],
    width = dotSize*dotRatio,
    height = dotSize*dotRatio,
    lineWidth = 1,
    opacity = dotOpacity, 
    depth = 1, 
    interpolate = True
    ))

I reached a limit of between 5,000 and 10,000 dots on screen before it fell over.

Have a look at noise type 1 here: etemp [PsychoPy]

Thank you for your advice.
I’ve tried to use ElementArrayStim before and really struggled; do you know if it can be used in PsychoPy’s builder? I’m new to python (and PsychoPy) so I’m trying to keep script writing to a minimum.

I’ve tried going into the coder window and written the following:

n_lines = 100
mu = 135
sigma = 25

pos = []
ori = []

for i in range(n_lines):

    x = random.uniform(-200, 200)
    y = random.uniform(-200, 200)

    pos.append([x, y])

    ori.append(np.random.normal(loc = 135, scale = 25))

Lines = visual.ElementArrayStim(
    win=win,
    units="pix",
    fieldPos=(0.0, 0.0),
    fieldSize=(1.0, 1.0),
    fieldShape='circle',
    nElements=n_lines,
    elementTex="sqr",
    elementMask=None,
    sfs=None,
    xys=pos,
    sizes=[15,2.5],
    oris=ori,
    colors=(1.0, 1.0, 1.0),
    colorSpace='rgb')

But I get an error message:

ImportError: sys.meta_path is None, Python is likely shutting down

Or, if I try:

ori = np.random.normal(loc = 135, scale = 25, size = 100)
pos = [np.random.uniform(-0.2,0.2,100),np.random.uniform(-0.2,0.2,100)]

I get the error message:

ValueError: New value should be one of these: ['Nx2']

UPDATE: here is the whole script I’m trying to run:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This experiment was created using PsychoPy3 Experiment Builder (v2020.2.10),
    on April 09, 2021, at 15:40
If you publish work using this script the most relevant publication is:

    Peirce J, Gray JR, Simpson S, MacAskill M, Höchenberger R, Sogo H, Kastman E, Lindeløv JK. (2019) 
        PsychoPy2: Experiments in behavior made easy Behav Res 51: 195. 
        https://doi.org/10.3758/s13428-018-01193-y

"""

from __future__ import absolute_import, division

from psychopy import locale_setup
from psychopy import prefs
from psychopy import sound, gui, visual, core, data, event, logging, clock
from psychopy.constants import (NOT_STARTED, STARTED, PLAYING, PAUSED,
                                STOPPED, FINISHED, PRESSED, RELEASED, FOREVER)

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
import sys  # to get file system encoding

from psychopy.hardware import keyboard

import random

# 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
psychopyVersion = '2020.2.10'
expName = 'OriStimGauss'  # from the Builder filename that created this script
expInfo = {'participant': '', 'session': '001'}
dlg = gui.DlgFromDict(dictionary=expInfo, sortKeys=False, title=expName)
if dlg.OK == False:
    core.quit()  # user pressed cancel
expInfo['date'] = data.getDateStr()  # add a simple timestamp
expInfo['expName'] = expName
expInfo['psychopyVersion'] = psychopyVersion

# Data file name stem = absolute path + name; later add .psyexp, .csv, .log, etc
filename = _thisDir + os.sep + u'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='C:\\Users\\r02mj20\\Desktop\\Office PC\\PhD\\UnderTheSea_Psychopy\\OriStimGauss_lastrun.py',
    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
frameTolerance = 0.001  # how close to onset before 'same' frame

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

# Setup the Window
win = visual.Window(
    size=[1536, 864], fullscr=True, screen=1, 
    winType='pyglet', allowGUI=False, allowStencil=False,
    monitor='testMonitor', color=[1.000,1.000,1.000], colorSpace='rgb',
    blendMode='avg', useFBO=True, 
    units='height')
# store frame rate of monitor if we can measure it
expInfo['frameRate'] = win.getActualFrameRate()
if expInfo['frameRate'] != None:
    frameDur = 1.0 / round(expInfo['frameRate'])
else:
    frameDur = 1.0 / 60.0  # could not measure, so guess

# create a default keyboard (e.g. to check for escape)
defaultKeyboard = keyboard.Keyboard()

# Initialize components for Routine "trial"
trialClock = core.Clock()

pos= [np.random.uniform(-0.2, 0.2,100),np.random.uniform(-0.2, 0.2,100)]
ori=(np.random.normal(loc = 135, scale = 25, size = 100))

polygon = visual.Polygon(
    win=win, name='polygon',
    edges=9999, size=(0.5, 0.5),
    ori=0, pos=(0, 0),
    lineWidth=5, lineColor=[-1.000,-1.000,-1.000], lineColorSpace='rgb',
    fillColor=[0.004,0.004,0.004], fillColorSpace='rgb',
    opacity=0.5, depth=-1.0, interpolate=True, autoLog=False)
GreenReef = visual.ImageStim(
    win=win,
    name='GreenReef', 
    image='C:\\Users\\r02mj20\\Desktop\\Office PC\\PhD\\UnderTheSea_Psychopy\\Artwork\\GreenReef.tif', mask=None,
    ori=0, pos=(0.7, 0.3), size=(0.2, 0.3),
    color=[1,1,1], colorSpace='rgb', opacity=1,
    flipHoriz=False, flipVert=False,
    texRes=128, interpolate=True, depth=-3.0, autoLog=False)
RedReef = visual.ImageStim(
    win=win,
    name='RedReef', 
    image='C:\\Users\\r02mj20\\Desktop\\Office PC\\PhD\\UnderTheSea_Psychopy\\Artwork\\RedReef.tif', mask=None,
    ori=0, pos=(-0.7, 0.3), size=(0.2, 0.3),
    color=[1,1,1], colorSpace='rgb', opacity=1,
    flipHoriz=False, flipVert=False,
    texRes=128, interpolate=True, depth=-4.0, autoLog=False)
Cross = visual.ShapeStim(
    win=win, name='Cross', vertices='cross',
    size=(0.05, 0.05),
    ori=0, pos=(0, 0),
    lineWidth=0.05, lineColor=[-1.000,-1.000,-1.000], lineColorSpace='rgb',
    fillColor=[-1.000,-1.000,-1.000], fillColorSpace='rgb',
    opacity=1, depth=-5.0, interpolate=True, autoLog=False)
Lines = visual.ElementArrayStim(
    win=win,
    units="pix",
    fieldPos=(0.0, 0.0),
    fieldSize=(1.0, 1.0),
    fieldShape='circle',
    nElements=100,
    elementTex="sqr",
    elementMask=None,
    sfs=None,
    xys=pos,
    sizes=[15,2.5],
    oris=ori,
    colors=(1.0, 1.0, 1.0),
    colorSpace='rgb')
    
# 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 "trial"-------
continueRoutine = True
# update component parameters for each repeat
Lines.setOris(ori)

# keep track of which components have finished
trialComponents = [polygon, GreenReef, RedReef, Cross, Lines]
for thisComponent in trialComponents:
    thisComponent.tStart = None
    thisComponent.tStop = None
    thisComponent.tStartRefresh = None
    thisComponent.tStopRefresh = None
    if hasattr(thisComponent, 'status'):
        thisComponent.status = NOT_STARTED
# reset timers
t = 0
_timeToFirstFrame = win.getFutureFlipTime(clock="now")
trialClock.reset(-_timeToFirstFrame)  # t0 is time of first possible flip
frameN = -1

# -------Run Routine "trial"-------
while continueRoutine:
    # get current time
    t = trialClock.getTime()
    tThisFlip = win.getFutureFlipTime(clock=trialClock)
    tThisFlipGlobal = win.getFutureFlipTime(clock=None)
    frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
    # update/draw components on each frame
       
    # *polygon* updates
    if polygon.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
        # keep track of start time/frame for later
        polygon.frameNStart = frameN  # exact frame index
        polygon.tStart = t  # local t and not account for scr refresh
        polygon.tStartRefresh = tThisFlipGlobal  # on global time
        win.timeOnFlip(polygon, 'tStartRefresh')  # time at next scr refresh
        polygon.setAutoDraw(True)
    
    # *GreenReef* updates
    if GreenReef.status == NOT_STARTED and tThisFlip >= 1.0-frameTolerance:
        # keep track of start time/frame for later
        GreenReef.frameNStart = frameN  # exact frame index
        GreenReef.tStart = t  # local t and not account for scr refresh
        GreenReef.tStartRefresh = tThisFlipGlobal  # on global time
        win.timeOnFlip(GreenReef, 'tStartRefresh')  # time at next scr refresh
        GreenReef.setAutoDraw(True)
    
    # *RedReef* updates
    if RedReef.status == NOT_STARTED and tThisFlip >= 1.0-frameTolerance:
        # keep track of start time/frame for later
        RedReef.frameNStart = frameN  # exact frame index
        RedReef.tStart = t  # local t and not account for scr refresh
        RedReef.tStartRefresh = tThisFlipGlobal  # on global time
        win.timeOnFlip(RedReef, 'tStartRefresh')  # time at next scr refresh
        RedReef.setAutoDraw(True)
    
    # *Cross* updates
    if Cross.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
        # keep track of start time/frame for later
        Cross.frameNStart = frameN  # exact frame index
        Cross.tStart = t  # local t and not account for scr refresh
        Cross.tStartRefresh = tThisFlipGlobal  # on global time
        win.timeOnFlip(Cross, 'tStartRefresh')  # time at next scr refresh
        Cross.setAutoDraw(True)
    if Cross.status == STARTED:
        # is it time to stop? (based on global clock, using actual start)
        if tThisFlipGlobal > Cross.tStartRefresh + 1-frameTolerance:
            # keep track of stop time/frame for later
            Cross.tStop = t  # not accounting for scr refresh
            Cross.frameNStop = frameN  # exact frame index
            win.timeOnFlip(Cross, 'tStopRefresh')  # time at next scr refresh
            Cross.setAutoDraw(False)
    
    # *Lines* updates
    if Lines.status == NOT_STARTED and tThisFlip >= 1.0-frameTolerance:
        # keep track of start time/frame for later
        Lines.frameNStart = frameN  # exact frame index
        Lines.tStart = t  # local t and not account for scr refresh
        Lines.tStartRefresh = tThisFlipGlobal  # on global time
        win.timeOnFlip(Lines, 'tStartRefresh')  # time at next scr refresh
        Lines.setAutoDraw(True)      
    
    # check for quit (typically the Esc key)
    if endExpNow or defaultKeyboard.getKeys(keyList=["escape"]):
        core.quit()
    
    # 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
    
    # 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)
thisExp.addData('polygon.started', polygon.tStartRefresh)
thisExp.addData('polygon.stopped', polygon.tStopRefresh)
thisExp.addData('GreenReef.started', GreenReef.tStartRefresh)
thisExp.addData('GreenReef.stopped', GreenReef.tStopRefresh)
thisExp.addData('RedReef.started', RedReef.tStartRefresh)
thisExp.addData('RedReef.stopped', RedReef.tStopRefresh)
thisExp.addData('Cross.started', Cross.tStartRefresh)
thisExp.addData('Cross.stopped', Cross.tStopRefresh)
thisExp.addData('Lines.started', Lines.tStartRefresh)
thisExp.addData('Lines.stopped', Lines.tStopRefresh)
# the Routine "trial" was not non-slip safe, so reset the non-slip timer
routineTimer.reset()

# Flip one final time so any remaining win.callOnFlip() 
# and win.timeOnFlip() tasks get executed before quitting
win.flip()

# these shouldn't be strictly necessary (should auto-save)
thisExp.saveAsWideText(filename+'.csv', delim='auto')
thisExp.saveAsPickle(filename)
logging.flush()
# make sure everything is closed down
thisExp.abort()  # or data files will save again on exit
win.close()
core.quit()

FURTHER UPDATE: I noticed I had xys = pos which is why I was getting the error:

ValueError: New value should be one of these: ['Nx2']

I’ve set xys = None and now I get the following error message:

####### Running: C:\Users\r02mj20\Desktop\PsychoPy_Exp\Full ori trial.py #######
pygame 1.9.6
Hello from the pygame community. https://www.pygame.org/contribute.html
0.7393     WARNING     We strongly recommend you activate the PTB sound engine in PsychoPy prefs as the preferred audio engine. Its timing is vastly superior. Your prefs are currently set to use ['sounddevice', 'PTB', 'pyo', 'pygame'] (in that order).
4.6233     WARNING     User requested fullscreen with size [1536  864], but screen is actually [1920, 1080]. Using actual size
Traceback (most recent call last):
  File "C:\Users\r02mj20\Desktop\PsychoPy_Exp\Full ori trial.py", line 218, in <module>
    Lines.setAutoDraw(True)      
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\psychopy\visual\basevisual.py", line 183, in setAutoDraw
    setAttribute(self, 'autoDraw', value, log)
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\psychopy\tools\attributetools.py", line 141, in setAttribute
    setattr(self, attrib, value)
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\psychopy\tools\attributetools.py", line 32, in __set__
    newValue = self.func(obj, value)
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\psychopy\visual\basevisual.py", line 161, in autoDraw
    iis = numpy.where(depthArray < self.depth)[0]
TypeError: '<' not supported between instances of 'float' and 'attributeSetter'
Exception ignored in: <bound method ImageStim.__del__ of <psychopy.visual.image.ImageStim object at 0x000002181DA4B048>>
Traceback (most recent call last):
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\psychopy\visual\image.py", line 238, in __del__
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\pyglet\gl\lib.py", line 97, in errcheck
ImportError: sys.meta_path is None, Python is likely shutting down
Exception ignored in: <bound method ImageStim.__del__ of <psychopy.visual.image.ImageStim object at 0x000002181DA4B080>>
Traceback (most recent call last):
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\psychopy\visual\image.py", line 238, in __del__
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\pyglet\gl\lib.py", line 97, in errcheck
ImportError: sys.meta_path is None, Python is likely shutting down
Exception ignored in: <bound method ElementArrayStim.__del__ of <psychopy.visual.elementarray.ElementArrayStim object at 0x000002181DA41EB8>>
Traceback (most recent call last):
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\psychopy\visual\elementarray.py", line 735, in __del__
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\psychopy\visual\basevisual.py", line 1020, in clearTextures
  File "C:\Users\r02mj20\AppData\Local\PsychoPy3\lib\site-packages\pyglet\gl\lib.py", line 97, in errcheck
ImportError: sys.meta_path is None, Python is likely shutting down
##### Experiment ended. #####