#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""This stimulus class defines a field of elements whose behaviour can be
independently controlled. Suitable for creating 'global form' stimuli or more
detailed random dot stimuli."""
# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).
# Ensure setting pyglet.options['debug_gl'] to False is done prior to any
# other calls to pyglet or pyglet submodules, otherwise it may not get picked
# up by the pyglet GL engine and have no effect.
# Shaders will work but require OpenGL2.0 drivers AND PyOpenGL3.0+
import pyglet
from ..colors import Color
pyglet.options['debug_gl'] = False
import ctypes
GL = pyglet.gl
import psychopy # so we can get the __path__
from psychopy import logging
from psychopy.visual import Window
# tools must only be imported *after* event or MovieStim breaks on win32
# (JWP has no idea why!)
from psychopy.tools.arraytools import val2array
from psychopy.tools.attributetools import attributeSetter, logAttrib, setAttribute
from psychopy.tools.monitorunittools import convertToPix
from psychopy.visual.helpers import setColor
from psychopy.visual.basevisual import MinimalStim, TextureMixin, ColorMixin
from . import globalVars
import numpy
[docs]class ElementArrayStim(MinimalStim, TextureMixin, ColorMixin):
"""This stimulus class defines a field of elements whose behaviour can
be independently controlled. Suitable for creating 'global form'
stimuli or more detailed random dot stimuli. This is a lazy-imported
class, therefore import using full path
`from psychopy.visual.elementarray import ElementArrayStim` when
inheriting from it.
This stimulus can draw thousands of elements without dropping a frame,
but in order to achieve this performance, uses several OpenGL extensions
only available on modern graphics cards (supporting OpenGL2.0).
See the ElementArray demo.
"""
def __init__(self,
win,
units=None,
fieldPos=(0.0, 0.0),
fieldSize=(1.0, 1.0),
fieldShape='circle',
nElements=100,
sizes=2.0,
xys=None,
rgbs=None,
colors=(1.0, 1.0, 1.0),
colorSpace='rgb',
opacities=1.0,
depths=0,
fieldDepth=0,
oris=0,
sfs=1.0,
contrs=1,
phases=0,
elementTex='sin',
elementMask='gauss',
texRes=48,
interpolate=True,
name=None,
autoLog=None,
maskParams=None):
"""
:Parameters:
win :
a :class:`~psychopy.visual.Window` object (required)
units : **None**, 'height', 'norm', 'cm', 'deg' or 'pix'
If None then the current units of the
:class:`~psychopy.visual.Window` will be used.
See :ref:`units` for explanation of other options.
nElements :
number of elements in the array.
"""
# what local vars are defined (these are the init params) for use by
# __repr__
self._initParams = dir()
self._initParams.remove('self')
super(ElementArrayStim, self).__init__(name=name, autoLog=False)
self.autoLog = False # until all params are set
self.win = win
# Not pretty (redefined later) but it works!
self.__dict__['texRes'] = texRes
self.__dict__['maskParams'] = maskParams
# unit conversions
if units != None and len(units):
self.units = units
else:
self.units = win.units
self.__dict__['fieldShape'] = fieldShape
self.nElements = nElements
# info for each element
self.__dict__['sizes'] = sizes
self.verticesBase = xys
self._needVertexUpdate = True
self._needColorUpdate = True
self._RGBAs = None
self.interpolate = interpolate
self.__dict__['fieldDepth'] = fieldDepth
self.__dict__['depths'] = depths
if self.win.winType == 'pygame':
raise TypeError('ElementArrayStim is not supported in a pygame context')
if not self.win._haveShaders:
raise Exception("ElementArrayStim requires shaders support"
" and floating point textures")
self.colorSpace = colorSpace
if rgbs != None:
msg = ("Use of the rgb argument to ElementArrayStim is deprecated"
". Please use colors and colorSpace args instead")
logging.warning(msg)
self.setColors(rgbs, colorSpace='rgb', log=False)
else:
self.setColors(colors, colorSpace=colorSpace, log=False)
# Deal with input for fieldpos and fieldsize
self.__dict__['fieldPos'] = val2array(fieldPos, False, False)
self.__dict__['fieldSize'] = val2array(fieldSize, False)
# create textures
self._texID = GL.GLuint()
GL.glGenTextures(1, ctypes.byref(self._texID))
self._maskID = GL.GLuint()
GL.glGenTextures(1, ctypes.byref(self._maskID))
self.setMask(elementMask, log=False)
self.texRes = texRes
self.setTex(elementTex, log=False)
self.setContrs(contrs, log=False)
# opacities is used by setRgbs, so this needs to be early
self.setOpacities(opacities, log=False)
self.setXYs(xys, log=False)
self.setOris(oris, log=False)
# set sizes before sfs (sfs may need it formatted)
self.setSizes(sizes, log=False)
self.setSfs(sfs, log=False)
self.setPhases(phases, log=False)
self._updateVertices()
# set autoLog now that params have been initialised
wantLog = autoLog is None and self.win.autoLog
self.__dict__['autoLog'] = autoLog or wantLog
if self.autoLog:
logging.exp("Created %s = %s" % (self.name, str(self)))
def _selectWindow(self, win):
# don't call switch if it's already the curr window
if win != globalVars.currWindow and win.winType == 'pyglet':
win.winHandle.switch_to()
globalVars.currWindow = win
def _makeNx2(self, value, acceptedInput=('scalar', 'Nx1', 'Nx2')):
"""Helper function to change input to Nx2 arrays
'scalar': int/float, 1x1 and 2x1.
'Nx1': vector of values for each element.
'Nx2': x-y pair for each element
"""
# Make into an array if not already
value = numpy.array(value, dtype=float)
# Check shape and transform if not appropriate
valShpElem = value.shape in [(self.nElements,), (self.nElements, 1)]
if 'scalar' in acceptedInput and value.shape in [(), (1,), (2,)]:
value = numpy.resize(value, [self.nElements, 2])
elif 'Nx1' in acceptedInput and valShpElem:
value.shape = (self.nElements, 1) # set to be 2D
value = value.repeat(2, 1) # repeat once on dim 1
elif 'Nx2' in acceptedInput and value.shape == (self.nElements, 2):
pass # all is good
else:
msg = 'New value should be one of these: '
raise ValueError(msg + str(acceptedInput))
return value
def _makeNx1(self, value, acceptedInput=('scalar', 'Nx1')):
"""Helper function to change input to Nx1 arrays
'scalar': int, 1x1 and 2x1.
'Nx1': vector of values for each element."""
# Make into an array if not already
value = numpy.array(value, dtype=float)
# Check shape and transform if not appropriate
valShpElem = value.shape in [(self.nElements,), (self.nElements, 1)]
if 'scalar' in acceptedInput and value.shape in [(), (1,)]:
value = value.repeat(self.nElements)
elif 'Nx1' in acceptedInput and valShpElem:
pass # all is good
else:
msg = 'New value should be one of these: '
raise ValueError(msg + str(acceptedInput))
return value
@attributeSetter
def xys(self, value):
"""The xy positions of the elements centres, relative to the
field centre. Values should be:
- None
- an array/list of Nx2 coordinates.
If value is None then the xy positions will be generated
automatically, based on the fieldSize and fieldPos. In this
case opacity will also be overridden by this function (it is
used to make elements outside the field invisible).
:ref:`operations <attrib-operations>` are supported.
"""
if value is None:
fsz = self.fieldSize
rand = numpy.random.rand
if self.fieldShape in ('sqr', 'square'):
# initialise a random array of X,Y
self.__dict__['xys'] = rand(self.nElements, 2) * fsz - (fsz / 2)
# gone outside the square
xxx = (self.xys[:, 0] + (fsz[0] / 2)) % fsz[0]
yyy = (self.xys[:, 1] + (fsz[1] / 2)) % fsz[1]
self.__dict__['xys'][:, 0] = xxx - (fsz[0] / 2)
self.__dict__['xys'][:, 1] = yyy - (fsz[1] / 2)
elif self.fieldShape == 'circle':
# take twice as many elements as we need (and cull the ones
# outside the circle)
# initialise a random array of X,Y
xys = rand(self.nElements * 2, 2) * fsz - (fsz / 2)
# gone outside the square
xys[:, 0] = ((xys[:, 0] + (fsz[0] / 2)) % fsz[0]) - (fsz[0] / 2)
xys[:, 1] = ((xys[:, 1] + (fsz[1] / 2)) % fsz[1]) - (fsz[1] / 2)
# use a circular envelope and flips dot to opposite edge
# if they fall beyond radius.
# NB always circular - uses fieldSize in X only
normxy = xys / (fsz / 2.0)
dotDist = numpy.sqrt((normxy[:, 0]**2.0 + normxy[:, 1]**2.0))
self.__dict__['xys'] = xys[dotDist < 1.0, :][0:self.nElements]
else:
self.__dict__['xys'] = self._makeNx2(value, ['Nx2'])
# to keep a record if we are to alter things later.
self._xysAsNone = value is None
self._needVertexUpdate = True
def setXYs(self, value=None, operation='', log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
setAttribute(self, 'xys', value, log, operation)
@attributeSetter
def fieldShape(self, value):
"""The shape of the array ('circle' or 'sqr').
Will only have effect if xys=None."""
self.__dict__['fieldShape'] = value
if self._xysAsNone:
self.xys = None # call attributeSetter
else:
logging.warning("Tried to set FieldShape but XYs were given "
"explicitly. This won't have any effect.")
@attributeSetter
def oris(self, value):
"""(Nx1 or a single value) The orientations of the elements. Oris
are in degrees, and can be greater than 360 and smaller than 0.
An ori of 0 is vertical, and increasing ori values are increasingly
clockwise.
:ref:`operations <attrib-operations>` are supported.
"""
self.__dict__['oris'] = self._makeNx1(value) # set self.oris
self._needVertexUpdate = True
def setOris(self, value, operation='', log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
# call attributeSetter
setAttribute(self, 'oris', value, log, operation)
@attributeSetter
def sfs(self, value):
"""The spatial frequency for each element. Should either be:
- a single value
- an Nx1 array/list
- an Nx2 array/list (spatial frequency of the element in X and Y).
If the units for the stimulus are 'pix' or 'norm' then the units of sf
are cycles per stimulus width. For units of 'deg' or 'cm' the units
are c/cm or c/deg respectively.
:ref:`operations <attrib-operations>` are supported.
"""
self.__dict__['sfs'] = self._makeNx2(value) # set self.sfs
self._needTexCoordUpdate = True
def setSfs(self, value, operation='', log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
# in the case of Nx1 list/array, setAttribute would fail if not this:
value = self._makeNx2(value)
# call attributeSetter
setAttribute(self, 'sfs', value, log, operation)
@attributeSetter
def opacities(self, value):
"""Set the opacity for each element.
Should either be a single value or an Nx1 array/list
:ref:`Operations <attrib-operations>` are supported.
"""
self.__dict__['opacities'] = self._makeNx1(value)
self._needColorUpdate = True
def setOpacities(self, value, operation='', log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
setAttribute(self, 'opacities', value, log,
operation) # call attributeSetter
@attributeSetter
def sizes(self, value):
"""Set the size for each element. Should either be:
- a single value
- an Nx1 array/list
- an Nx2 array/list
:ref:`Operations <attrib-operations>` are supported.
"""
self.__dict__['sizes'] = self._makeNx2(value)
self._needVertexUpdate = True
self._needTexCoordUpdate = True
def setSizes(self, value, operation='', log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
# in the case of Nx1 list/array, setAttribute would fail if not this:
value = self._makeNx2(value)
# call attributeSetter
setAttribute(self, 'sizes', value, log, operation)
@attributeSetter
def phases(self, value):
"""The spatial phase of the texture on each element. Should either be:
- a single value
- an Nx1 array/list
- an Nx2 array/list (for separate X and Y phase)
:ref:`Operations <attrib-operations>` are supported.
"""
self.__dict__['phases'] = self._makeNx2(value)
self._needTexCoordUpdate = True
def setPhases(self, value, operation='', log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
# in the case of Nx1 list/array, setAttribute would fail if not this:
value = self._makeNx2(value)
setAttribute(self, 'phases', value, log,
operation) # call attributeSetter
def setRgbs(self, value, operation=''):
"""DEPRECATED (as of v1.74.00). Please use setColors() instead.
"""
self.setColors(value, operation)
@property
def colors(self):
"""Specifying the color(s) of the elements.
Should be Nx1 (different intensities), Nx3 (different colors) or
1x3 (for a single color).
See other stimuli (e.g. :ref:`GratingStim.color`) for more info
on the color attribute which essentially works the same on all
PsychoPy stimuli. Remember that they describe just this case but
here you can provide a list of colors - one color for each element.
Use ``setColors()`` if you want to set colors and colorSpace
simultaneously or use operations on colors.
"""
if hasattr(self, '_colors'):
# Return array of rendered colors
return self._colors.render(self.colorSpace)
@colors.setter
def colors(self, value):
# Create blank array of colors
self._colors = Color(value, self.colorSpace, self.contrast)
self._needColorUpdate = True
def setColors(self, colors, colorSpace=None, operation='', log=None):
"""See ``color`` for more info on the color parameter and
``colorSpace`` for more info in the colorSpace parameter.
"""
self.colorSpace = colorSpace
self.colors = colors
@property
def opacity(self):
if hasattr(self, "_opacity"):
return self._opacity
@opacity.setter
def opacity(self, value):
self._opacity = value
if hasattr(self, "_colors"):
# Set the alpha value of each color to be the desired opacity
self._colors.alpha = value
@attributeSetter
def contrs(self, value):
"""The contrasts of the elements, ranging -1 to +1. Should either be:
- a single value
- an Nx1 array/list
:ref:`Operations <attrib-operations>` are supported.
"""
# Convert to an Nx1 numpy array
value = self._makeNx1(value)
# If colors is too short, extend it
self._colors.rgb = numpy.resize(self._colors.rgb, (len(value), 3))
# Set
self._colors.contrast = value
# Store value and update
self.__dict__['contrs'] = value
self._needColorUpdate = True
def setContrs(self, value, operation='', log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
setAttribute(self, 'contrs', value, log, operation)
@attributeSetter
def fieldPos(self, value):
""":ref:`x,y-pair <attrib-xy>`.
Set the centre of the array of elements.
:ref:`Operations <attrib-operations>` are supported.
"""
self.__dict__['fieldPos'] = val2array(value, False, False)
self._needVertexUpdate = True
def setFieldPos(self, value, operation='', log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
setAttribute(self, 'fieldPos', value, log, operation)
def setPos(self, newPos=None, operation='', units=None, log=None):
"""Obsolete - users should use setFieldPos or instead of setPos.
"""
logging.error("User called ElementArrayStim.setPos(pos). "
"Use ElementArrayStim.setFieldPos(pos) instead.")
@attributeSetter
def fieldSize(self, value):
"""Scalar or :ref:`x,y-pair <attrib-xy>`.
The size of the array of elements. This will be overridden by
setting explicit xy positions for the elements.
:ref:`Operations <attrib-operations>` are supported.
"""
self.__dict__['fieldSize'] = val2array(value, False)
# to reflect new settings, overriding individual xys
self.setXYs(log=False)
def setFieldSize(self, value, operation='', log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
setAttribute(self, 'fieldSize', value, log, operation)
def draw(self, win=None):
"""Draw the stimulus in its relevant window. You must call
this method after every MyWin.update() if you want the
stimulus to appear on that frame and then update the screen
again.
"""
if win is None:
win = self.win
self._selectWindow(win)
if self._needVertexUpdate:
self._updateVertices()
if self._needColorUpdate:
self.updateElementColors()
if self._needTexCoordUpdate:
self.updateTextureCoords()
# scale the drawing frame and get to centre of field
GL.glPushMatrix() # push before drawing, pop after
# push the data for client attributes
GL.glPushClientAttrib(GL.GL_CLIENT_ALL_ATTRIB_BITS)
# GL.glLoadIdentity()
self.win.setScale('pix')
cpcd = ctypes.POINTER(ctypes.c_double)
GL.glColorPointer(4, GL.GL_DOUBLE, 0,
self._RGBAs.ctypes.data_as(cpcd))
GL.glVertexPointer(3, GL.GL_DOUBLE, 0,
self.verticesPix.ctypes.data_as(cpcd))
# setup the shaderprogram
_prog = self.win._progSignedTexMask
GL.glUseProgram(_prog)
# set the texture to be texture unit 0
GL.glUniform1i(GL.glGetUniformLocation(_prog, b"texture"), 0)
# mask is texture unit 1
GL.glUniform1i(GL.glGetUniformLocation(_prog, b"mask"), 1)
# bind textures
GL.glActiveTexture(GL.GL_TEXTURE1)
GL.glBindTexture(GL.GL_TEXTURE_2D, self._maskID)
GL.glEnable(GL.GL_TEXTURE_2D)
GL.glActiveTexture(GL.GL_TEXTURE0)
GL.glBindTexture(GL.GL_TEXTURE_2D, self._texID)
GL.glEnable(GL.GL_TEXTURE_2D)
# setup client texture coordinates first
GL.glClientActiveTexture(GL.GL_TEXTURE0)
GL.glTexCoordPointer(2, GL.GL_DOUBLE, 0, self._texCoords.ctypes)
GL.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY)
GL.glClientActiveTexture(GL.GL_TEXTURE1)
GL.glTexCoordPointer(2, GL.GL_DOUBLE, 0, self._maskCoords.ctypes)
GL.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY)
GL.glEnableClientState(GL.GL_COLOR_ARRAY)
GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
GL.glDrawArrays(GL.GL_QUADS, 0, self.verticesPix.shape[0] * 4)
# unbind the textures
GL.glActiveTexture(GL.GL_TEXTURE1)
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
GL.glDisable(GL.GL_TEXTURE_2D)
# main texture
GL.glActiveTexture(GL.GL_TEXTURE0)
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
GL.glDisable(GL.GL_TEXTURE_2D)
# disable states
GL.glDisableClientState(GL.GL_COLOR_ARRAY)
GL.glDisableClientState(GL.GL_VERTEX_ARRAY)
GL.glDisableClientState(GL.GL_TEXTURE_COORD_ARRAY)
GL.glUseProgram(0)
GL.glPopClientAttrib()
GL.glPopMatrix()
def _updateVertices(self):
"""Sets Stim.verticesPix from fieldPos.
"""
# Handle the orientation, size and location of
# each element in native units
radians = 0.017453292519943295
# so we can do matrix rotation of coords we need shape=[n*4,3]
# but we'll convert to [n,4,3] after matrix math
verts = numpy.zeros([self.nElements * 4, 3], 'd')
wx = -self.sizes[:, 0] * numpy.cos(self.oris[:] * radians) / 2
wy = self.sizes[:, 0] * numpy.sin(self.oris[:] * radians) / 2
hx = self.sizes[:, 1] * numpy.sin(self.oris[:] * radians) / 2
hy = self.sizes[:, 1] * numpy.cos(self.oris[:] * radians) / 2
# X vals of each vertex relative to the element's centroid
verts[0::4, 0] = -wx - hx
verts[1::4, 0] = +wx - hx
verts[2::4, 0] = +wx + hx
verts[3::4, 0] = -wx + hx
# Y vals of each vertex relative to the element's centroid
verts[0::4, 1] = -wy - hy
verts[1::4, 1] = +wy - hy
verts[2::4, 1] = +wy + hy
verts[3::4, 1] = -wy + hy
# set of positions across elements
positions = self.xys + self.fieldPos
# depth
verts[:, 2] = self.depths + self.fieldDepth
# rotate, translate, scale by units
if positions.shape[0] * 4 == verts.shape[0]:
positions = positions.repeat(4, 0)
verts[:, :2] = convertToPix(vertices=verts[:, :2], pos=positions,
units=self.units, win=self.win)
verts = verts.reshape([self.nElements, 4, 3])
# assign to self attribute; make sure it's contiguous
self.__dict__['verticesPix'] = numpy.require(verts,
requirements=['C'])
self._needVertexUpdate = False
# ----------------------------------------------------------------------
def updateElementColors(self):
"""Create a new array of self._RGBAs based on self.rgbs.
Not needed by the user (simple call setColors())
For element arrays the self.rgbs values correspond to one
element so this function also converts them to be one for
each vertex of each element.
"""
N = self.nElements
_RGBAs = numpy.zeros([len(self.verticesPix), 4], 'd')
_RGBAs[:,:] = self._colors.render('rgba1')
_RGBAs[:, -1] = self.opacities.reshape([N, ])
self._RGBAs = _RGBAs.reshape([len(self.verticesPix), 1, 4]).repeat(4, 1)
self._needColorUpdate = False
def updateTextureCoords(self):
"""Create a new array of self._maskCoords
"""
N = self.nElements
self._maskCoords = numpy.array([[1, 0], [0, 0], [0, 1], [1, 1]],
'd').reshape([1, 4, 2])
self._maskCoords = self._maskCoords.repeat(N, 0)
# for the main texture
# sf is dependent on size (openGL default)
if self.units in ['norm', 'pix', 'height']:
L = (-self.sfs[:, 0] / 2) - self.phases[:, 0] + 0.5
R = (+self.sfs[:, 0] / 2) - self.phases[:, 0] + 0.5
T = (+self.sfs[:, 1] / 2) - self.phases[:, 1] + 0.5
B = (-self.sfs[:, 1] / 2) - self.phases[:, 1] + 0.5
else:
# we should scale to become independent of size
L = (-self.sfs[:, 0] * self.sizes[:, 0] / 2
- self.phases[:, 0] + 0.5)
R = (+self.sfs[:, 0] * self.sizes[:, 0] / 2
- self.phases[:, 0] + 0.5)
T = (+self.sfs[:, 1] * self.sizes[:, 1] / 2
- self.phases[:, 1] + 0.5)
B = (-self.sfs[:, 1] * self.sizes[:, 1] / 2
- self.phases[:, 1] + 0.5)
# self._texCoords=numpy.array([[1,1],[1,0],[0,0],[0,1]],
# 'd').reshape([1,4,2])
self._texCoords = (numpy.concatenate([[R, B], [L, B], [L, T], [R, T]])
.transpose().reshape([N, 4, 2]).astype('d'))
self._texCoords = numpy.ascontiguousarray(self._texCoords)
self._needTexCoordUpdate = False
@attributeSetter
def elementTex(self, value):
"""The texture, to be used by all elements (e.g. 'sin', 'sqr',.. ,
'myTexture.tif', numpy.ones([48,48])). Avoid this
during time-critical points in your script. Uploading new textures
to the graphics card can be time-consuming.
"""
self.__dict__['tex'] = value
self._createTexture(value, id=self._texID, pixFormat=GL.GL_RGB,
stim=self, res=self.texRes,
maskParams=self.maskParams)
def setTex(self, value, log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
setAttribute(self, 'elementTex', value, log)
@attributeSetter
def depth(self, value):
"""(Nx1) list/array of ints.
The depths of the elements, relative the overall depth
of the field (fieldDepth).
:ref:`operations <attrib-operations>` are supported.
"""
self.__dict__['depth'] = value
self._updateVertices()
@attributeSetter
def fieldDepth(self, value):
"""Int. The depth of the field (will be added to the depths
of the elements).
:ref:`operations <attrib-operations>` are supported.
"""
self.__dict__['fieldDepth'] = value
self._updateVertices()
@attributeSetter
def elementMask(self, value):
"""The mask, to be used by all elements (e.g. 'circle', 'gauss',... ,
'myTexture.tif', numpy.ones([48,48])).
This is just a synonym for ElementArrayStim.mask. See doc there.
"""
self.mask = value
def __del__(self):
# remove textures from graphics card to prevent OpenGl memory leak
try:
self.clearTextures()
except (ImportError, ModuleNotFoundError, TypeError):
pass # has probably been garbage-collected already