-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathdot.py
726 lines (628 loc) · 31 KB
/
dot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
# -*- coding: utf-8 -*-
"""
Created on Sun May 19 22:19:21 2024
@author: user
"""
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""This stimulus class defines a field of dots with an update rule that
determines how they change on every call to the .draw() method.
"""
# 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).
# Bugfix by Andrew Schofield.
# 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.
# 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.
# Provide a visible wrapper function to refresh all the dot locations so that
# the whole field can be more easily refreshed between trials.
# 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
pyglet.options['debug_gl'] = False
import ctypes
GL = pyglet.gl
import psychopy # so we can get the __path__
from psychopy import logging
# tools must only be imported *after* event or MovieStim breaks on win32
# (JWP has no idea why!)
from psychopy.tools.attributetools import attributeSetter, setAttribute
from psychopy.tools.arraytools import val2array
from psychopy.visual.basevisual import (BaseVisualStim, ColorMixin,
ContainerMixin, WindowMixin)
from psychopy.layout import Size
import numpy as np
# some constants
_piOver2 = np.pi / 2.
_piOver180 = np.pi / 180.
_2pi = 2 * np.pi
class DotStim(BaseVisualStim, ColorMixin, ContainerMixin):
"""This stimulus class defines a field of dots with an update rule that
determines how they change on every call to the .draw() method. This is
a lazy-imported class, therefore import using full path
`from psychopy.visual.dot import DotStim` when inheriting from it.
This single class can be used to generate a wide variety of dot motion
types. For a review of possible types and their pros and cons see Scase,
Braddick & Raymond (1996). All six possible motions they describe can be
generated with appropriate choices of the `signalDots` (which determines
whether signal dots are the 'same' or 'different' on each frame),
`noiseDots` (which determines the locations of the noise dots on each frame)
and the `dotLife` (which determines for how many frames the dot will
continue before being regenerated).
The default settings (as of v1.70.00) is for the noise dots to have
identical velocity but random direction and signal dots remain the 'same'
(once a signal dot, always a signal dot).
For further detail about the different configurations see :ref:`dots` in the
Builder Components section of the documentation.
If further customisation is required, then the DotStim should be subclassed
and its _update_dotsXY and _newDotsXY methods overridden.
The maximum number of dots that can be drawn is limited by system
performance.
Attributes
----------
fieldShape : str
*'sqr'* or 'circle'. Defines the envelope used to present the dots. If
changed while drawing, dots outside new envelope will be respawned.
dotSize : float
Dot size specified in pixels (overridden if `element` is specified).
:ref:`operations <attrib-operations>` are supported.
dotLife : int
Number of frames each dot lives for (-1=infinite). Dot lives are
initiated randomly from a uniform distribution from 0 to dotLife. If
changed while drawing, the lives of all dots will be randomly initiated
again.
signalDots : str
If 'same' then the signal and noise dots are constant. If 'different'
then the choice of which is signal and which is noise gets randomised on
each frame. This corresponds to Scase et al's (1996) categories of RDK.
noiseDots : str
Determines the behaviour of the noise dots, taken directly from Scase et
al's (1996) categories. For 'position', noise dots take a random
position every frame. For 'direction' noise dots follow a random, but
constant direction. For 'walk' noise dots vary their direction every
frame, but keep a constant speed.
element : object
This can be any object that has a ``.draw()`` method and a
``.setPos([x,y])`` method (e.g. a GratingStim, TextStim...)!! DotStim
assumes that the element uses pixels as units. ``None`` defaults to
dots.
fieldPos : array_like
Specifying the location of the centre of the stimulus using a
:ref:`x,y-pair <attrib-xy>`. See e.g. :class:`.ShapeStim` for more
documentation/examples on how to set position.
:ref:`operations <attrib-operations>` are supported.
fieldSize : array_like
Specifying the size of the field of dots using a
:ref:`x,y-pair <attrib-xy>`. See e.g. :class:`.ShapeStim` for more
documentation/examples on how to set position.
:ref:`operations <attrib-operations>` are supported.
coherence : float
Change the coherence (%) of the DotStim. This will be rounded according
to the number of dots in the stimulus.
dir : float
Direction of the coherent dots in degrees. :ref:`operations
<attrib-operations>` are supported.
speed : float
Speed of the dots (in *units*/frame). :ref:`operations
<attrib-operations>` are supported.
"""
def __init__(self,
win,
units='',
nDots=1,
coherence=0.5,
fieldPos=(0.0, 0.0),
fieldSize=(1.0, 1.0),
fieldShape='sqr',
fieldAnchor="center",
dotSize=2.0,
dotLife=3,
dir=0.0,
speed=0.5,
rgb=None,
color=(1.0, 1.0, 1.0),
colorSpace='rgb',
opacity=None,
contrast=1.0,
depth=0,
element=None,
signalDots='same',
noiseDots='direction',
name=None,
autoLog=None):
"""
Parameters
----------
win : window.Window
Window this stimulus is associated with.
units : str
Units to use.
nDots : int
Number of dots to present in the field.
coherence : float
Proportion of dots which are coherent. This value can be set using
the `coherence` property after initialization.
fieldPos : array_like
(x,y) or [x,y] position of the field. This value can be set using
the `fieldPos` property after initialization.
fieldSize : array_like, int or float
(x,y) or [x,y] or single value (applied to both dimensions). Sizes
can be negative and can extend beyond the window. This value can be
set using the `fieldSize` property after initialization.
fieldShape : str
Defines the envelope used to present the dots. If changed while
drawing by setting the `fieldShape` property, dots outside new
envelope will be respawned., valid values are 'square', 'sqr' or
'circle'.
dotSize : array_like or float
Size of the dots. If given an array, the sizes of individual dots
will be set. The array must have length `nDots`. If a single value
is given, all dots will be set to the same size.
dotLife : int
Lifetime of a dot in frames. Dot lives are initiated randomly from a
uniform distribution from 0 to dotLife. If changed while drawing,
the lives of all dots will be randomly initiated again. A value of
-1 results in dots having an infinite lifetime. This value can be
set using the `dotLife` property after initialization.
dir : float
Direction of the coherent dots in degrees. At 0 degrees, coherent
dots will move from left to right. Increasing the angle will rotate
the direction counter-clockwise. This value can be set using the
`dir` property after initialization.
speed : float
Speed of the dots (in *units* per frame). This value can be set
using the `speed` property after initialization.
rgb : array_like, optional
Color of the dots in form (r, g, b) or [r, g, b]. **Deprecated**,
use `color` instead.
color : array_like or str
Color of the dots in form (r, g, b) or [r, g, b].
colorSpace : str
Colorspace to use.
opacity : float
Opacity of the dots from 0.0 to 1.0.
contrast : float
Contrast of the dots 0.0 to 1.0. This value is simply multiplied by
the `color` value.
depth : float
**Deprecated**, depth is now controlled simply by drawing order.
element : object
This can be any object that has a ``.draw()`` method and a
``.setPos([x,y])`` method (e.g. a GratingStim, TextStim...)!!
DotStim assumes that the element uses pixels as units.
``None`` defaults to dots.
signalDots : str
If 'same' then the signal and noise dots are constant. If different
then the choice of which is signal and which is noise gets
randomised on each frame. This corresponds to Scase et al's (1996)
categories of RDK. This value can be set using the `signalDots`
property after initialization.
noiseDots : str
Determines the behaviour of the noise dots, taken directly from
Scase et al's (1996) categories. For 'position', noise dots take a
random position every frame. For 'direction' noise dots follow a
random, but constant direction. For 'walk' noise dots vary their
direction every frame, but keep a constant speed. This value can be
set using the `noiseDots` property after initialization.
name : str, optional
Optional name to use for logging.
autoLog : bool
Enable automatic logging.
"""
# what local vars are defined (these are the init params) for use by
# __repr__
self._initParams = __builtins__['dir']()
self._initParams.remove('self')
super(DotStim, self).__init__(win, units=units, name=name,
autoLog=False) # set at end of init
self.nDots = nDots
# pos and size are ambiguous for dots so DotStim explicitly has
# fieldPos = pos, fieldSize=size and then dotSize as additional param
self.fieldPos = fieldPos # self.pos is also set here
self.fieldSize = val2array(fieldSize, False) # self.size is also set
if type(dotSize) in (tuple, list):
self.dotSize = np.array(dotSize)
else:
self.dotSize = dotSize
if self.win.useRetina:
self.dotSize *= 2 # double dot size to make up for 1/2-size pixels
self.fieldShape = fieldShape
self.__dict__['dir'] = dir
self.speed = speed
self.element = element
self.dotLife = dotLife
self.signalDots = signalDots
if rgb != None:
logging.warning("Use of rgb arguments to stimuli are deprecated."
" Please use color and colorSpace args instead")
self.colorSpace = 'rgba'
self.color = rgb
else:
self.colorSpace = colorSpace
self.color = color
self.opacity = opacity
self.contrast = float(contrast)
self.depth = depth
# initialise the dots themselves - give them all random dir and then
# fix the first n in the array to have the direction specified
self.coherence = coherence # using the attributeSetter
self.noiseDots = noiseDots
# initialise a random array of X,Y
self.vertices = self._verticesBase = self._dotsXY = self._newDotsXY(self.nDots)
# all dots have the same speed
self._dotsSpeed = np.ones(self.nDots, dtype=float) * self.speed
# abs() means we can ignore the -1 case (no life)
self._dotsLife = np.abs(dotLife) * np.random.rand(self.nDots)
# pre-allocate array for flagging dead dots
self._deadDots = np.zeros(self.nDots, dtype=bool)
# set directions (only used when self.noiseDots='direction')
self._dotsDir = np.random.rand(self.nDots) * _2pi
self._dotsDir[self._signalDots] = self.dir * _piOver180
self._update_dotsXY()
self.anchor = fieldAnchor
# 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 set(self, attrib, val, op='', log=None):
"""DEPRECATED: DotStim.set() is obsolete and may not be supported
in future versions of PsychoPy. Use the specific method for each
parameter instead (e.g. setFieldPos(), setCoherence()...).
"""
self._set(attrib, val, op, log=log)
@attributeSetter
def fieldShape(self, fieldShape):
"""*'sqr'* or 'circle'. Defines the envelope used to present the dots.
If changed while drawing, dots outside new envelope will be respawned.
"""
self.__dict__['fieldShape'] = fieldShape
@property
def anchor(self):
return WindowMixin.anchor.fget(self)
@anchor.setter
def anchor(self, value):
WindowMixin.anchor.fset(self, value)
def setAnchor(self, value, log=None):
setAttribute(self, 'anchor', value, log)
@property
def dotSize(self):
"""Float specified in pixels (overridden if `element` is specified).
:ref:`operations <attrib-operations>` are supported."""
if hasattr(self, "_dotSize"):
return getattr(self._dotSize, 'pix')[0]
@dotSize.setter
def dotSize(self, value):
self._dotSize = Size(value, units='pix', win=self.win)
@attributeSetter
def dotLife(self, dotLife):
"""Int. Number of frames each dot lives for (-1=infinite).
Dot lives are initiated randomly from a uniform distribution
from 0 to dotLife. If changed while drawing, the lives of all
dots will be randomly initiated again.
:ref:`operations <attrib-operations>` are supported.
"""
self.__dict__['dotLife'] = dotLife
self._dotsLife = abs(self.dotLife) * np.random.rand(self.nDots)
@attributeSetter
def signalDots(self, signalDots):
"""str - 'same' or *'different'*
If 'same' then the signal and noise dots are constant. If different
then the choice of which is signal and which is noise gets
randomised on each frame. This corresponds to Scase et al's (1996)
categories of RDK.
"""
self.__dict__['signalDots'] = signalDots
@attributeSetter
def noiseDots(self, noiseDots):
"""str - *'direction'*, 'position' or 'walk'
Determines the behaviour of the noise dots, taken directly from
Scase et al's (1996) categories. For 'position', noise dots take a
random position every frame. For 'direction' noise dots follow a
random, but constant direction. For 'walk' noise dots vary their
direction every frame, but keep a constant speed.
"""
self.__dict__['noiseDots'] = noiseDots
self.coherence = self.coherence # update using attributeSetter
@attributeSetter
def element(self, element):
"""*None* or a visual stimulus object
This can be any object that has a ``.draw()`` method and a
``.setPos([x,y])`` method (e.g. a GratingStim, TextStim...)!!
DotStim assumes that the element uses pixels as units.
``None`` defaults to dots.
See `ElementArrayStim` for a faster implementation of this idea.
"""
self.__dict__['element'] = element
@attributeSetter
def fieldPos(self, pos):
"""Specifying the location of the centre of the stimulus
using a :ref:`x,y-pair <attrib-xy>`.
See e.g. :class:`.ShapeStim` for more documentation / examples
on how to set position.
:ref:`operations <attrib-operations>` are supported.
"""
# Isn't there a way to use BaseVisualStim.pos.__doc__ as docstring
# here?
self.pos = pos # using BaseVisualStim. we'll store this as both
self.__dict__['fieldPos'] = self.pos
def setFieldPos(self, val, op='', 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', val, log, op) # calls attributeSetter
def setPos(self, newPos=None, operation='', units=None, log=None):
"""Obsolete - users should use setFieldPos instead of setPos
"""
logging.error("User called DotStim.setPos(pos). "
"Use DotStim.SetFieldPos(pos) instead.")
def setFieldSize(self, val, op='', 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', val, log, op) # calls attributeSetter
@attributeSetter
def fieldSize(self, size):
"""Specifying the size of the field of dots using a
:ref:`x,y-pair <attrib-xy>`. See e.g. :class:`.ShapeStim` for more
documentation/examples on how to set position.
:ref:`operations <attrib-operations>` are supported.
"""
# Isn't there a way to use BaseVisualStim.pos.__doc__ as docstring
# here?
self.size = size # using BaseVisualStim. we'll store this as both
self.__dict__['fieldSize'] = self.size
@attributeSetter
def coherence(self, coherence):
"""Scalar between 0 and 1.
Change the coherence (%) of the DotStim. This will be rounded according
to the number of dots in the stimulus.
:ref:`operations <attrib-operations>` are supported.
"""
if not 0 <= coherence <= 1:
raise ValueError('DotStim.coherence must be between 0 and 1')
_cohDots = coherence * self.nDots
self.__dict__['coherence'] = round(_cohDots) /self.nDots
self._signalDots = np.zeros(self.nDots, dtype=bool)
self._signalDots[0:int(self.coherence * self.nDots)] = True
# for 'direction' method we need to update the direction of the number
# of signal dots immediately, but for other methods it will be done
# during updateXY
# NB - AJS Actually you need to do this for 'walk' also
# otherwise would be signal dots adopt random directions when the become
# sinal dots in later trails
if self.noiseDots in ('direction', 'position', 'walk'):
self._dotsDir = np.random.rand(self.nDots) * _2pi
self._dotsDir[self._signalDots] = self.dir * _piOver180
def setFieldCoherence(self, val, op='', 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, 'coherence', val, log, op) # calls attributeSetter
@attributeSetter
def dir(self, dir):
"""float (degrees). direction of the coherent dots. :ref:`operations
<attrib-operations>` are supported.
"""
# check which dots are signal before setting new dir
signalDots = self._dotsDir == (self.dir * _piOver180)
self.__dict__['dir'] = dir
# dots currently moving in the signal direction also need to update
# their direction
self._dotsDir[signalDots] = self.dir * _piOver180
def setDir(self, val, op='', 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, 'dir', val, log, op)
@attributeSetter
def speed(self, speed):
"""float. speed of the dots (in *units*/frame). :ref:`operations
<attrib-operations>` are supported.
"""
self.__dict__['speed'] = speed
def setSpeed(self, val, op='', 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, 'speed', val, log, op)
def draw(self, win=None):
"""Draw the stimulus in its relevant window. You must call this method
after every MyWin.flip() if you want the stimulus to appear on that
frame and then update the screen again.
Parameters
----------
win : window.Window, optional
Window to draw dots to. If `None`, dots will be drawn to the parent
window.
"""
if win is None:
win = self.win
self._selectWindow(win)
self._update_dotsXY()
GL.glPushMatrix() # push before drawing, pop after
# draw the dots
if self.element is None:
win.setScale('pix')
GL.glPointSize(self.dotSize)
# load Null textures into multitexteureARB - they modulate with
# glColor
GL.glActiveTexture(GL.GL_TEXTURE0)
GL.glEnable(GL.GL_TEXTURE_2D)
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
GL.glActiveTexture(GL.GL_TEXTURE1)
GL.glEnable(GL.GL_TEXTURE_2D)
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
CPCD = ctypes.POINTER(ctypes.c_double)
GL.glVertexPointer(2, GL.GL_DOUBLE, 0,
self.verticesPix.ctypes.data_as(CPCD))
GL.glColor4f(*self._foreColor.render('rgba1'))
GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
GL.glDrawArrays(GL.GL_POINTS, 0, self.nDots)
GL.glDisableClientState(GL.GL_VERTEX_ARRAY)
else:
# we don't want to do the screen scaling twice so for each dot
# subtract the screen centre
initialDepth = self.element.depth
for pointN in range(0, self.nDots):
_p = self.verticesPix[pointN, :] + self.fieldPos
self.element.setPos(_p)
self.element.draw()
# reset depth before going to next frame
self.element.setDepth(initialDepth)
GL.glPopMatrix()
def _newDotsXY(self, nDots):
"""Returns a uniform spread of dots, according to the `fieldShape` and
`fieldSize`.
Parameters
----------
nDots : int
Number of dots to sample.
Returns
-------
ndarray
Nx2 array of X and Y positions of dots.
Examples
--------
Create a new array of dot positions::
dots = self._newDots(nDots)
"""
if self.fieldShape == 'circle':
length = np.sqrt(np.random.uniform(0, 1, (nDots,)))
angle = np.random.uniform(0., _2pi, (nDots,))
newDots = np.zeros((nDots, 2))
newDots[:, 0] = length * np.cos(angle)
newDots[:, 1] = length * np.sin(angle)
newDots *= self.fieldSize * .5
else:
newDots = np.random.uniform(-0.5, 0.5, size = (nDots, 2)) * self.fieldSize
return newDots
#updating the position of the dots for the square aparatus according to the direction of the movement
def getRandPosInSquareSide(self,sideNum):
xy = np.zeros((1, 2))
if sideNum==0:
xy[:,0] = -0.5*self.fieldSize[0] #set x
xy[:,1] = np.random.uniform(-0.5, 0.5, size = None) * self.fieldSize[1] #set y
elif sideNum==1:
xy[:,0] = np.random.uniform(-0.5, 0.5, size = None) * self.fieldSize[0] #set x
xy[:,1] = -0.5*self.fieldSize[1] #set y
if sideNum==2:
xy[:,0] = 0.5*self.fieldSize[0] #set x
xy[:,1] = np.random.uniform(-0.5, 0.5, size = None) * self.fieldSize[1] #set y
elif sideNum==3:
xy[:,0] = np.random.uniform(-0.5, 0.5, size = None) * self.fieldSize[0] #set x
xy[:,1] = 0.5*self.fieldSize[1] #set y
return xy
# main for updating dots (also here is where the dot in circle are updated)
def _update_OutOfBoundXY(self, outofbounds):
nOutOfBounds = outofbounds.sum()
allDir = self._dotsDir[outofbounds];
newDots = np.zeros((nOutOfBounds, 2))
if self.fieldShape=='sqr':
for i in range(nOutOfBounds):
currDir = allDir[i]%_2pi;
modAngle = currDir % _piOver2
side = currDir//_piOver2
oddsInFirstSide = 1/(1+np.tan(modAngle))
isIn2ndSide = np.random.rand()>=oddsInFirstSide
sideEnter = (side+isIn2ndSide) % 4; #which of 4 sides of the square a new dot enters
newDots[i,:] = self.getRandPosInSquareSide(sideEnter)
return newDots
elif self.fieldShape=='circle':
for i in range(nOutOfBounds):
currDir = allDir[i]%_2pi;
entryAngle = currDir+np.pi;
ShiftFromEntryAngleOnEdge = np.arcsin(np.random.uniform(-1, 1, size = None))
angleEntryOnCircEdge = entryAngle+ShiftFromEntryAngleOnEdge;
newDots[i,0] = np.cos(angleEntryOnCircEdge)*0.5*self.fieldSize[0]
newDots[i,1] = np.sin(angleEntryOnCircEdge)*0.5*self.fieldSize[1]
return newDots
def refreshDots(self):
"""Callable user function to choose a new set of dots."""
self.vertices = self._verticesBase = self._dotsXY = self._newDotsXY(self.nDots)
# Don't allocate another array if the new number of dots is equal to
# the last.
if self.nDots != len(self._deadDots):
self._deadDots = np.zeros(self.nDots, dtype=bool)
def _update_dotsXY(self):
"""The user shouldn't call this - its gets done within draw().
"""
# Find dead dots, update positions, get new positions for
# dead and out-of-bounds
# renew dead dots
if self.dotLife > 0: # if less than zero ignore it
# decrement. Then dots to be reborn will be negative
self._dotsLife -= 1
self._deadDots[:] = (self._dotsLife <= 0)
self._dotsLife[self._deadDots] = self.dotLife
else:
self._deadDots[:] = False
# update XY based on speed and dir
# NB self._dotsDir is in radians, but self.dir is in degs
# update which are the noise/signal dots
if self.signalDots == 'different':
# **up to version 1.70.00 this was the other way around,
# not in keeping with Scase et al**
# noise and signal dots change identity constantly
np.random.shuffle(self._dotsDir)
# and then update _signalDots from that
self._signalDots = (self._dotsDir == (self.dir * _piOver180))
# update the locations of signal and noise; 0 radians=East!
reshape = np.reshape
if self.noiseDots == 'walk':
# noise dots are ~self._signalDots
sig = np.random.rand(np.sum(~self._signalDots))
self._dotsDir[~self._signalDots] = sig * _2pi
# then update all positions from dir*speed
cosDots = reshape(np.cos(self._dotsDir), (self.nDots,))
sinDots = reshape(np.sin(self._dotsDir), (self.nDots,))
self._verticesBase[:, 0] += self.speed * cosDots
self._verticesBase[:, 1] += self.speed * sinDots
elif self.noiseDots == 'direction':
# simply use the stored directions to update position
cosDots = reshape(np.cos(self._dotsDir), (self.nDots,))
sinDots = reshape(np.sin(self._dotsDir), (self.nDots,))
self._verticesBase[:, 0] += self.speed * cosDots
self._verticesBase[:, 1] += self.speed * sinDots
elif self.noiseDots == 'position':
# update signal dots
sd = self._signalDots
sdSum = self._signalDots.sum()
cosDots = reshape(np.cos(self._dotsDir[sd]), (sdSum,))
sinDots = reshape(np.sin(self._dotsDir[sd]), (sdSum,))
self._verticesBase[sd, 0] += self.speed * cosDots
self._verticesBase[sd, 1] += self.speed * sinDots
# update noise dots
self._deadDots[:] = self._deadDots + (~self._signalDots)
# handle boundaries of the field
if self.fieldShape in (None, 'square', 'sqr'):
out0 = (np.abs(self._verticesBase[:, 0]) > .5 * self.fieldSize[0])
out1 = (np.abs(self._verticesBase[:, 1]) > .5 * self.fieldSize[1])
outofbounds = out0 + out1
else:
# transform to a normalised circle (radius = 1 all around)
# then to polar coords to check
# the normalised XY position (where radius should be < 1)
normXY = self._verticesBase / .5 / self.fieldSize
# add out-of-bounds to those that need replacing
outofbounds = np.hypot(normXY[:, 0], normXY[:, 1]) > 1.
# update any dead dots
nDead = self._deadDots.sum()
if nDead:
self._verticesBase[self._deadDots, :] = self._newDotsXY(nDead)
# 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.
nOutOfBounds = outofbounds.sum()
if nOutOfBounds:
# self._verticesBase[outofbounds, :] = self._newDotsXY(nOutOfBounds)
self._verticesBase[outofbounds, :] = self._update_OutOfBoundXY(outofbounds)
self.vertices = self._verticesBase / self.fieldSize
# update the pixel XY coordinates in pixels (using _BaseVisual class)
self._updateVertices()