Advertisement
Guest User

Untitled

a guest
Jul 17th, 2019
85
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 21.53 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. """This stimulus class defines a field of dots with an update rule that
  5. determines how they change on every call to the .draw() method.
  6. """
  7.  
  8. # Part of the PsychoPy library
  9. # Copyright (C) 2018 Jonathan Peirce
  10. # Distributed under the terms of the GNU General Public License (GPL).
  11.  
  12. # Bugfix by Andrew Schofield.
  13. # Replaces out of bounds but still live dots at opposite edge of aperture instead of randomly within the field. This stops the concentration of dots at one side of field when lifetime is long.
  14. # Update the dot direction immediately for 'walk' as otherwise when the coherence varies some signal dots will inherit the random directions of previous walking dots.
  15. # Provide a visible wrapper function to refresh all the dot locations so that the whole field can be more easily refreshed between trials.
  16.  
  17. from __future__ import absolute_import, division, print_function
  18.  
  19. from builtins import str
  20. from builtins import range
  21.  
  22. # Ensure setting pyglet.options['debug_gl'] to False is done prior to any
  23. # other calls to pyglet or pyglet submodules, otherwise it may not get picked
  24. # up by the pyglet GL engine and have no effect.
  25. # Shaders will work but require OpenGL2.0 drivers AND PyOpenGL3.0+
  26. import pyglet
  27. pyglet.options['debug_gl'] = False
  28. import ctypes
  29. GL = pyglet.gl
  30.  
  31. import psychopy # so we can get the __path__
  32. from psychopy import logging
  33.  
  34. # tools must only be imported *after* event or MovieStim breaks on win32
  35. # (JWP has no idea why!)
  36. from psychopy.tools.attributetools import attributeSetter, setAttribute
  37. from psychopy.tools.arraytools import val2array
  38. from psychopy.tools.monitorunittools import cm2pix, deg2pix
  39. from psychopy.visual.basevisual import (BaseVisualStim, ColorMixin,
  40. ContainerMixin)
  41.  
  42. import numpy
  43. from numpy import pi
  44.  
  45.  
  46. class DotStim(BaseVisualStim, ColorMixin, ContainerMixin):
  47. """This stimulus class defines a field of dots with an update rule
  48. that determines how they change on every call to the .draw() method.
  49.  
  50. This single class can be used to generate a wide variety of
  51. dot motion types. For a review of possible types and their pros and
  52. cons see Scase, Braddick & Raymond (1996). All six possible motions
  53. they describe can be generated with appropriate choices of the
  54. signalDots (which determines whether signal dots are the 'same' or
  55. 'different' on each frame), noiseDots (which determines the locations
  56. of the noise dots on each frame) and the dotLife (which determines
  57. for how many frames the dot will continue before being regenerated).
  58.  
  59. The default settings (as of v1.70.00) is for the noise dots to have
  60. identical velocity but random direction and signal dots remain the
  61. 'same' (once a signal dot, always a signal dot).
  62.  
  63. For further detail about the different configurations see :ref:`dots`
  64. in the Builder Components section of the documentation.
  65.  
  66. If further customisation is required, then the DotStim should be
  67. subclassed and its _update_dotsXY and _newDotsXY methods overridden.
  68. """
  69.  
  70. def __init__(self,
  71. win,
  72. units='',
  73. nDots=1,
  74. coherence=0.5,
  75. fieldPos=(0.0, 0.0),
  76. fieldSize=(1.0, 1.0),
  77. fieldShape='sqr',
  78. dotSize=2.0,
  79. dotLife=3,
  80. dir=0.0,
  81. speed=0.5,
  82. rgb=None,
  83. color=(1.0, 1.0, 1.0),
  84. colorSpace='rgb',
  85. opacity=1.0,
  86. contrast=1.0,
  87. depth=0,
  88. element=None,
  89. signalDots='same',
  90. noiseDots='direction',
  91. name=None,
  92. autoLog=None):
  93. """
  94. :Parameters:
  95.  
  96. fieldSize : (x,y) or [x,y] or single value (applied to both
  97. dimensions). Sizes can be negative and can extend beyond
  98. the window.
  99. """
  100. # what local vars are defined (these are the init params) for use by
  101. # __repr__
  102. self._initParams = __builtins__['dir']()
  103. self._initParams.remove('self')
  104.  
  105. super(DotStim, self).__init__(win, units=units, name=name,
  106. autoLog=False) # set at end of init
  107.  
  108. self.nDots = nDots
  109. # pos and size are ambiguous for dots so DotStim explicitly has
  110. # fieldPos = pos, fieldSize=size and then dotSize as additional param
  111. self.fieldPos = fieldPos # self.pos is also set here
  112. self.fieldSize = val2array(fieldSize, False) # self.size is also set
  113. if type(dotSize) in [tuple, list]:
  114. self.dotSize = numpy.array(dotSize)
  115. else:
  116. self.dotSize = dotSize
  117. if self.win.useRetina:
  118. self.dotSize *= 2 # double dot size to make up for 1/2-size pixels
  119. self.fieldShape = fieldShape
  120. self.__dict__['dir'] = dir
  121. self.speed = speed
  122. self.element = element
  123. self.dotLife = dotLife
  124. self.signalDots = signalDots
  125. self.opacity = float(opacity)
  126. self.contrast = float(contrast)
  127.  
  128. self.useShaders = False # not needed for dots?
  129. self.colorSpace = colorSpace
  130. if rgb != None:
  131. logging.warning("Use of rgb arguments to stimuli are deprecated."
  132. " Please use color and colorSpace args instead")
  133. self.setColor(rgb, colorSpace='rgb', log=False)
  134. else:
  135. self.setColor(color, log=False)
  136.  
  137. self.depth = depth
  138.  
  139. # initialise the dots themselves - give them all random dir and then
  140. # fix the first n in the array to have the direction specified
  141.  
  142. self.coherence = coherence # using the attributeSetter
  143. self.noiseDots = noiseDots
  144.  
  145. # initialise a random array of X,Y
  146. self. _verticesBase = self._dotsXY = self._newDotsXY(self.nDots)
  147. # all dots have the same speed
  148. self._dotsSpeed = numpy.ones(self.nDots, 'f') * self.speed
  149. # abs() means we can ignore the -1 case (no life)
  150. self._dotsLife = abs(dotLife) * numpy.random.rand(self.nDots)
  151. # numpy.random.shuffle(self._signalDots) # not really necessary
  152. # set directions (only used when self.noiseDots='direction')
  153. self._dotsDir = numpy.random.rand(self.nDots) * 2 * pi
  154. self._dotsDir[self._signalDots] = self.dir * pi / 180
  155.  
  156. self._update_dotsXY()
  157.  
  158. # set autoLog now that params have been initialised
  159. wantLog = autoLog is None and self.win.autoLog
  160. self.__dict__['autoLog'] = autoLog or wantLog
  161. if self.autoLog:
  162. logging.exp("Created %s = %s" % (self.name, str(self)))
  163.  
  164. def set(self, attrib, val, op='', log=None):
  165. """DEPRECATED: DotStim.set() is obsolete and may not be supported
  166. in future versions of PsychoPy. Use the specific method for each
  167. parameter instead (e.g. setFieldPos(), setCoherence()...).
  168. """
  169. self._set(attrib, val, op, log=log)
  170.  
  171. @attributeSetter
  172. def fieldShape(self, fieldShape):
  173. """*'sqr'* or 'circle'. Defines the envelope used to present the dots.
  174. If changed while drawing, dots outside new envelope will be respawned.
  175. """
  176. self.__dict__['fieldShape'] = fieldShape
  177.  
  178. @attributeSetter
  179. def dotSize(self, dotSize):
  180. """Float specified in pixels (overridden if `element` is specified).
  181. :ref:`operations <attrib-operations>` are supported."""
  182. self.__dict__['dotSize'] = dotSize
  183.  
  184. @attributeSetter
  185. def dotLife(self, dotLife):
  186. """Int. Number of frames each dot lives for (-1=infinite).
  187. Dot lives are initiated randomly from a uniform distribution
  188. from 0 to dotLife. If changed while drawing, the lives of all
  189. dots will be randomly initiated again.
  190.  
  191. :ref:`operations <attrib-operations>` are supported.
  192. """
  193. self.__dict__['dotLife'] = dotLife
  194. self._dotsLife = abs(self.dotLife) * numpy.random.rand(self.nDots)
  195.  
  196. @attributeSetter
  197. def signalDots(self, signalDots):
  198. """str - 'same' or *'different'*
  199. If 'same' then the signal and noise dots are constant. If different
  200. then the choice of which is signal and which is noise gets
  201. randomised on each frame. This corresponds to Scase et al's (1996)
  202. categories of RDK.
  203. """
  204. self.__dict__['signalDots'] = signalDots
  205.  
  206. @attributeSetter
  207. def noiseDots(self, noiseDots):
  208. """Str. *'direction'*, 'position' or 'walk'
  209. Determines the behaviour of the noise dots, taken directly from
  210. Scase et al's (1996) categories. For 'position', noise dots take a
  211. random position every frame. For 'direction' noise dots follow a
  212. random, but constant direction. For 'walk' noise dots vary their
  213. direction every frame, but keep a constant speed.
  214. """
  215. self.__dict__['noiseDots'] = noiseDots
  216. self.coherence = self.coherence # update using attributeSetter
  217.  
  218. @attributeSetter
  219. def element(self, element):
  220. """*None* or a visual stimulus object
  221. This can be any object that has a ``.draw()`` method and a
  222. ``.setPos([x,y])`` method (e.g. a GratingStim, TextStim...)!!
  223. DotStim assumes that the element uses pixels as units.
  224. ``None`` defaults to dots.
  225.  
  226. See `ElementArrayStim` for a faster implementation of this idea.
  227. """
  228. self.__dict__['element'] = element
  229.  
  230. @attributeSetter
  231. def fieldPos(self, pos):
  232. """Specifying the location of the centre of the stimulus
  233. using a :ref:`x,y-pair <attrib-xy>`.
  234. See e.g. :class:`.ShapeStim` for more documentation / examples
  235. on how to set position.
  236.  
  237. :ref:`operations <attrib-operations>` are supported.
  238. """
  239. # Isn't there a way to use BaseVisualStim.pos.__doc__ as docstring
  240. # here?
  241. self.pos = pos # using BaseVisualStim. we'll store this as both
  242. self.__dict__['fieldPos'] = self.pos
  243.  
  244. def setFieldPos(self, val, op='', log=None):
  245. """Usually you can use 'stim.attribute = value' syntax instead,
  246. but use this method if you need to suppress the log message
  247. """
  248. setAttribute(self, 'fieldPos', val, log, op) # calls attributeSetter
  249.  
  250. def setPos(self, newPos=None, operation='', units=None, log=None):
  251. """Obsolete - users should use setFieldPos instead of setPos
  252. """
  253. logging.error("User called DotStim.setPos(pos). "
  254. "Use DotStim.SetFieldPos(pos) instead.")
  255.  
  256. def setFieldSize(self, val, op='', log=None):
  257. """Usually you can use 'stim.attribute = value' syntax instead,
  258. but use this method if you need to suppress the log message
  259. """
  260. setAttribute(self, 'fieldSize', val, log, op) # calls attributeSetter
  261.  
  262. @attributeSetter
  263. def fieldSize(self, size):
  264. """Specifying the size of the field of dots using a
  265. :ref:`x,y-pair <attrib-xy>`.
  266. See e.g. :class:`.ShapeStim` for more documentation /
  267. examples on how to set position.
  268.  
  269. :ref:`operations <attrib-operations>` are supported.
  270. """
  271. # Isn't there a way to use BaseVisualStim.pos.__doc__ as docstring
  272. # here?
  273. self.size = size # using BaseVisualStim. we'll store this as both
  274. self.__dict__['fieldSize'] = self.size
  275.  
  276. @attributeSetter
  277. def coherence(self, coherence):
  278. """Scalar between 0 and 1.
  279.  
  280. Change the coherence (%) of the DotStim. This will be rounded
  281. according to the number of dots in the stimulus.
  282.  
  283. :ref:`operations <attrib-operations>` are supported.
  284. """
  285. if not 0 <= coherence <= 1:
  286. raise ValueError('DotStim.coherence must be between 0 and 1')
  287. _cohDots = coherence * self.nDots
  288. self.__dict__['coherence'] = round(_cohDots)/self.nDots
  289. self._signalDots = numpy.zeros(self.nDots, dtype=bool)
  290. self._signalDots[0:int(self.coherence * self.nDots)] = True
  291. # for 'direction' method we need to update the direction of the number
  292. # of signal dots immediately, but for other methods it will be done
  293. # during updateXY
  294. #:::::::::::::::::::: AJS Actually you need to do this for 'walk' also otherwise
  295. #would be signal dots adopt random directions when the become sinal dots in later trails
  296. if self.noiseDots in ['direction', 'position','walk']:
  297. self._dotsDir = numpy.random.rand(self.nDots) * 2 * pi
  298. self._dotsDir[self._signalDots] = self.dir * pi / 180
  299.  
  300. def setFieldCoherence(self, val, op='', log=None):
  301. """Usually you can use 'stim.attribute = value' syntax instead,
  302. but use this method if you need to suppress the log message
  303. """
  304. setAttribute(self, 'coherence', val, log, op) # calls attributeSetter
  305.  
  306. @attributeSetter
  307. def dir(self, dir):
  308. """float (degrees). direction of the coherent dots.
  309. :ref:`operations <attrib-operations>` are supported.
  310. """
  311. # check which dots are signal before setting new dir
  312. signalDots = self._dotsDir == (self.dir * pi / 180)
  313. self.__dict__['dir'] = dir
  314.  
  315. # dots currently moving in the signal direction also need to update
  316. # their direction
  317. self._dotsDir[signalDots] = self.dir * pi / 180
  318.  
  319. def setDir(self, val, op='', log=None):
  320. """Usually you can use 'stim.attribute = value' syntax instead,
  321. but use this method if you need to suppress the log message
  322. """
  323. setAttribute(self, 'dir', val, log, op)
  324.  
  325. @attributeSetter
  326. def speed(self, speed):
  327. """float. speed of the dots (in *units*/frame).
  328. :ref:`operations <attrib-operations>` are supported.
  329. """
  330. self.__dict__['speed'] = speed
  331.  
  332. def setSpeed(self, val, op='', log=None):
  333. """Usually you can use 'stim.attribute = value' syntax instead,
  334. but use this method if you need to suppress the log message
  335. """
  336. setAttribute(self, 'speed', val, log, op)
  337.  
  338. def draw(self, win=None):
  339. """Draw the stimulus in its relevant window. You must call
  340. this method after every MyWin.flip() if you want the
  341. stimulus to appear on that frame and then update the screen again.
  342. """
  343. if win is None:
  344. win = self.win
  345. self._selectWindow(win)
  346.  
  347. self._update_dotsXY()
  348.  
  349. GL.glPushMatrix() # push before drawing, pop after
  350.  
  351. # draw the dots
  352. if self.element is None:
  353. win.setScale('pix')
  354. GL.glPointSize(self.dotSize)
  355.  
  356. # load Null textures into multitexteureARB - they modulate with
  357. # glColor
  358. GL.glActiveTexture(GL.GL_TEXTURE0)
  359. GL.glEnable(GL.GL_TEXTURE_2D)
  360. GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
  361. GL.glActiveTexture(GL.GL_TEXTURE1)
  362. GL.glEnable(GL.GL_TEXTURE_2D)
  363. GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
  364.  
  365. CPCD = ctypes.POINTER(ctypes.c_double)
  366. GL.glVertexPointer(2, GL.GL_DOUBLE, 0,
  367. self.verticesPix.ctypes.data_as(CPCD))
  368. desiredRGB = self._getDesiredRGB(self.rgb, self.colorSpace,
  369. self.contrast)
  370.  
  371. GL.glColor4f(desiredRGB[0], desiredRGB[1], desiredRGB[2],
  372. self.opacity)
  373. GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
  374. GL.glDrawArrays(GL.GL_POINTS, 0, self.nDots)
  375. GL.glDisableClientState(GL.GL_VERTEX_ARRAY)
  376. else:
  377. # we don't want to do the screen scaling twice so for each dot
  378. # subtract the screen centre
  379. initialDepth = self.element.depth
  380. for pointN in range(0, self.nDots):
  381. _p = self.verticesPix[pointN, :] + self.fieldPos
  382. self.element.setPos(_p)
  383. self.element.draw()
  384. # reset depth before going to next frame
  385. self.element.setDepth(initialDepth)
  386. GL.glPopMatrix()
  387.  
  388. def _newDotsXY(self, nDots):
  389. """Returns a uniform spread of dots, according to the
  390. fieldShape and fieldSize
  391.  
  392. usage::
  393.  
  394. dots = self._newDots(nDots)
  395.  
  396. """
  397. # make more dots than we need and only use those within the circle
  398. if self.fieldShape == 'circle':
  399. while True:
  400. # repeat until we have enough; fetch twice as many as needed
  401. new = numpy.random.uniform(-1, 1, [nDots * 2, 2])
  402. inCircle = (numpy.hypot(new[:, 0], new[:, 1]) < 1)
  403. if sum(inCircle) >= nDots:
  404. return new[inCircle, :][:nDots, :] * self.fieldSize * 0.5
  405. else:
  406. return numpy.random.uniform(-0.5*self.fieldSize[0],
  407. 0.5*self.fieldSize[1], [nDots, 2])
  408.  
  409. def refreshDots(self):
  410. """Callable user function to choose a new set of dots"""
  411. self._verticesBase = self._dotsXY = self._newDotsXY(self.nDots)
  412.  
  413. def _update_dotsXY(self):
  414. """The user shouldn't call this - its gets done within draw().
  415. """
  416.  
  417. # Find dead dots, update positions, get new positions for
  418. # dead and out-of-bounds
  419. # renew dead dots
  420. if self.dotLife > 0: # if less than zero ignore it
  421. # decrement. Then dots to be reborn will be negative
  422. self._dotsLife -= 1
  423. dead = (self._dotsLife <= 0.0)
  424. self._dotsLife[dead] = self.dotLife
  425. else:
  426. dead = numpy.zeros(self.nDots, dtype=bool)
  427.  
  428. # update XY based on speed and dir
  429. # NB self._dotsDir is in radians, but self.dir is in degs
  430. # update which are the noise/signal dots
  431. if self.signalDots == 'different':
  432. # **up to version 1.70.00 this was the other way around,
  433. # not in keeping with Scase et al**
  434. # noise and signal dots change identity constantly
  435. numpy.random.shuffle(self._dotsDir)
  436. # and then update _signalDots from that
  437. self._signalDots = (self._dotsDir == (self.dir * pi / 180))
  438.  
  439. # update the locations of signal and noise; 0 radians=East!
  440. reshape = numpy.reshape
  441. if self.noiseDots == 'walk':
  442. # noise dots are ~self._signalDots
  443. sig = numpy.random.rand((~self._signalDots).sum())
  444. self._dotsDir[~self._signalDots] = sig * pi * 2
  445. # then update all positions from dir*speed
  446. cosDots = reshape(numpy.cos(self._dotsDir), (self.nDots,))
  447. sinDots = reshape(numpy.sin(self._dotsDir), (self.nDots,))
  448. self._verticesBase[:, 0] += self.speed * cosDots
  449. self._verticesBase[:, 1] += self.speed * sinDots
  450. elif self.noiseDots == 'direction':
  451. # simply use the stored directions to update position
  452. cosDots = reshape(numpy.cos(self._dotsDir), (self.nDots,))
  453. sinDots = reshape(numpy.sin(self._dotsDir), (self.nDots,))
  454. self._verticesBase[:, 0] += self.speed * cosDots
  455. self._verticesBase[:, 1] += self.speed * sinDots
  456. elif self.noiseDots == 'position':
  457. # update signal dots
  458. sd = self._signalDots
  459. sdSum = self._signalDots.sum()
  460. cosDots = reshape(numpy.cos(self._dotsDir[sd]), (sdSum,))
  461. sinDots = reshape(numpy.sin(self._dotsDir[sd]), (sdSum,))
  462. self._verticesBase[sd, 0] += self.speed * cosDots
  463. self._verticesBase[sd, 1] += self.speed * sinDots
  464. # update noise dots
  465. dead = dead + (~self._signalDots) # just create new ones
  466.  
  467. # handle boundaries of the field
  468. if self.fieldShape in (None, 'square', 'sqr'):
  469. #dead0 = (numpy.abs(self._verticesBase[:, 0]) > 0.5)
  470. #dead1 = (numpy.abs(self._verticesBase[:, 1]) > 0.5)
  471. #dead = dead + dead0 + dead1
  472. out0 = (numpy.abs(self._verticesBase[:, 0]) > 0.5*self.fieldSize[0])
  473. out1 = (numpy.abs(self._verticesBase[:, 1]) > 0.5*self.fieldSize[1])
  474. outofbounds = out0 + out1
  475.  
  476. elif self.fieldShape == 'circle':
  477. #outofbounds=None
  478. # transform to a normalised circle (radius = 1 all around)
  479. # then to polar coords to check
  480. # the normalised XY position (where radius should be < 1)
  481. normXY = self._verticesBase / 0.5 / self.fieldSize
  482. # add out-of-bounds to those that need replacing
  483. #dead+= (numpy.hypot(normXY[:, 0], normXY[:, 1]) > 1)
  484. outofbounds = (numpy.hypot(normXY[:, 0], normXY[:, 1]) > 1)
  485.  
  486. # update any dead dots
  487. if sum(dead):
  488. self._verticesBase[dead, :] = self._newDotsXY(sum(dead))
  489. #self._verticesBase[dead, :] = -self._verticesBase[dead,:]
  490.  
  491. # Reposition any dots that have gone out of bounds. Net effect is to place dot one step inside the boundary on the other side of the aperture.
  492. if sum(outofbounds):
  493. self._verticesBase[outofbounds, :] = self._newDotsXY(sum(outofbounds))
  494. #wind the dots back one step and store as tempary values
  495. # if self.noiseDots == 'position':
  496. # tempvert0=self._verticesBase[sd,0]-self.speed * cosDots
  497. # tempvert1=self._verticesBase[sd,1]-self.speed * sinDots
  498. # else:
  499. # tempvert0=self._verticesBase[:,0]-self.speed * cosDots
  500. # tempvert1=self._verticesBase[:,1]-self.speed * sinDots
  501. # #reflect the position of the dots about the origine of the dot field
  502. # self._verticesBase[outofbounds, 0] = -tempvert0[outofbounds]
  503. # self._verticesBase[outofbounds, 1] = -tempvert1[outofbounds]
  504.  
  505. # update the pixel XY coordinates in pixels (using _BaseVisual class)
  506. self._updateVertices()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement