1 """
2 Sheets for simulating a moving eye.
3
4 This module provides two classes, ShiftingGeneratorSheet, and
5 SaccadeController, that can be used to simulate a moving eye,
6 controlled by topographic neural activity from structures like the
7 superior colliculus.
8
9 ShiftingGeneratorSheet is a subclass of GeneratorSheet that accepts a
10 saccade command on the 'Saccade' port in the form of a tuple:
11 (amplitude,direction), specified in degrees. It shifts its sheet
12 bounds in response to this command, keeping the centroid of the bounds
13 within a prespecified boundingregion.
14
15 SaccadeController is a subclass of CFSheet that accepts CF projections
16 and decodes its resulting activity into a saccade command suitable for
17 controlling a ShiftingGeneratorSheet.
18
19
20 $Id: saccade.py 11316 2010-07-27 17:52:53Z ceball $
21 """
22 __version__ = '$Revision: 11316 $'
23
24 from numpy import sin,cos,pi,array,asarray,argmax,zeros,\
25 nonzero,take,random
26
27 import param
28
29 from topo.base.cf import CFSheet
30 from topo.base.simulation import PeriodicEventSequence,FunctionEvent
31 from topo.base.boundingregion import BoundingBox,BoundingRegionParameter
32 from topo.coordmapper.basic import CoordinateMapperFn, IdentityMF
33 from topo.sheet import SequenceGeneratorSheet
34 from topo.misc import util
35
36
37
38
39
40
42 """
43 Return the sheet coords of the (weighted) centroid of sheet activity.
44
45 If the activity argument is not None, then it is used instead
46 of sheet.activity. If the sheet activity is all zero, the
47 centroid of the sheet bounds is returned.
48 """
49
50 if activity is None:
51 activity = sheet.activity
52
53 ys = sheet.sheet_rows()
54 xs = sheet.sheet_cols()
55
56 xy = array([(x,y) for y in reversed(ys) for x in xs])
57 a = activity.flat
58
59
60
61 idxs = nonzero(a > threshold)[0]
62 if not len(idxs):
63 return sheet.bounds.centroid()
64 return util.centroid(take(xy,idxs,axis=0),take(a,idxs))
65
66
68 """
69 Sample from the sheet activity as if it were a probability distribution.
70
71 Returns the sheet coordinates of the sampled unit. If
72 activity is not None, it is used instead of sheet.activity.
73 """
74
75 if activity is None:
76 activity = sheet.activity
77 idx = util.weighted_sample_idx(activity.ravel())
78 r,c = util.idx2rowcol(idx,activity.shape)
79
80 return sheet.matrix2sheet(r,c)
81
82
84 """
85 Returns the sheet coordinates of the mode (highest value) of
86 the sheet activity.
87 """
88
89
90
91
92
93
94
95 if activity is None:
96 activity = sheet.activity
97 idx = argmax(activity.flat)
98 r,c = util.idx2rowcol(idx,activity.shape)
99 return sheet.matrix2sheet(r,c)
100
101
102
104 """
105 Sheet that decodes activity on CFProjections into a saccade command.
106
107 This class accepts CF-projected input and computes its activity
108 like a normal CFSheet, then decodes that activity into a saccade
109 amplitude and direction as would be specified by activity in the
110 superior colliculi. The X dimension of activity corresponds to
111 amplitude, the Y dimension to direction. The activity is decoded
112 to a single (x,y) point according to the parameter decode_method.
113
114 From this (x,y) point an (amplitude,direction) pair, specified in
115 degrees, is computed using the parameters amplitude_scale and
116 direction scale. That pair is then sent out on the 'Saccade'
117 output port.
118
119 NOTE: Non-linear mappings for saccade commands, as in Ottes, et
120 al (below), are assumed to be provided using the coord_mapperg
121 parameter of the incoming CFProjection.
122
123 References:
124 Ottes, van Gisbergen, Egglermont. 1986. Visuomotor fields of the
125 superior colliculus: a quantitative model. Vision Research;
126 26(6): 857-73.
127 """
128
129
130
131
132 amplitude_scale = param.Number(default=120,doc="""
133 Scale factor for saccade command amplitude, expressed in
134 degrees per unit of sheet. Indicates how large a saccade is
135 represented by the x-component of the command input.""")
136
137 direction_scale = param.Number(default=180,doc="""
138 Scale factor for saccade command direction, expressed in
139 degrees per unit of sheet. Indicates what direction of saccade
140 is represented by the y-component of the command input.""")
141
142
143 decode_fn = param.Callable(default=activity_centroid,
144 instantiate=False,doc="""
145 The function for extracting a single point from sheet activity.
146 Should take a sheet as the first argument, and return (x,y).""")
147
148 command_mapper = param.ClassSelector(CoordinateMapperFn,default=IdentityMF(),
149 doc="""
150 A CoordinateMapperFn that will be applied to the command vector extracted
151 from the sheet activity.""")
152
153 src_ports = ['Activity','Saccade']
154
155
173
174
175
176
177
179 """
180 A GeneratorSheet that takes an extra input on port 'Saccade'
181 that specifies a saccade command as a tuple (amplitude,direction),
182 indicating the relative size and direction of the saccade in
183 degrees. The parameter visual_angle_scale defines the
184 relationship between degrees and sheet coordinates. The parameter
185 saccade bounds limits the region within which the saccades may occur.
186 """
187
188 visual_angle_scale = param.Number(default=90,doc="""
189 The scale factor determining the visual angle subtended by this sheet, in
190 degrees per unit of sheet.""")
191
192 saccade_bounds = BoundingRegionParameter(default=BoundingBox(radius=1.0),doc="""
193 The bounds for saccades. Saccades are constrained such that the centroid of the
194 sheet bounds remains within this region.""")
195
196 generate_on_shift = param.Boolean(default=True,doc="""
197 Whether to generate a new pattern when a shift occurs.""")
198
199 fixation_jitter = param.Number(default=0,doc="""
200 Standard deviation of Gaussian fixation jitter.""")
201 fixation_jitter_period = param.Number(default=10,doc="""
202 Period, in time units, indicating how often the eye jitters.
203 """)
204
205 dest_ports = ["Trigger","Saccade"]
206 src_ports = ['Activity','Position']
207
211
220
226
231
232
233 - def shift(self,amplitude,direction,generate=None):
234 """
235 Shift the bounding box by the given amplitude and
236 direction.
237
238 Amplitude and direction are specified in degrees, and will be
239 converted using the sheet's visual_angle_scale
240 parameter. Negative directions are always downward, regardless
241 of whether the amplitude is positive (rightword) or negative
242 (leftward). I.e. straight-down = -90, straight up = +90.
243
244 The generate argument indicates whether or not to generate
245 output after shifting. If generate is None, then the value of
246 the sheet's generate_on_shift parameter will be used.
247 """
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263 assert not self._out_of_bounds()
264
265
266 radius = amplitude/self.visual_angle_scale
267
268
269 if radius < 0.0:
270 direction *= -1
271
272 self._translate(radius,direction)
273
274 if self._out_of_bounds():
275 self._find_saccade_in_bounds(radius,direction)
276
277 self.fixation_point = self.bounds.centroid()
278
279 if generate is None:
280 generate = self.generate_on_shift
281
282 if generate:
283 self.generate()
284
286 """
287 Move the bounds toward the fixation point.
288
289 Moves the bounds toward the fixation point specified in
290 self.fixation_point, potentially with noise as specified by
291 the parameter self.fixation_jitter.
292 """
293 self.debug("Refixating.")
294
295 if self.fixation_jitter > 0:
296 jitter_vec = random.normal(0,self.fixation_jitter,(2,))
297 else:
298 jitter_vec = zeros((2,))
299
300 fix = asarray(self.fixation_point)
301 pos = asarray(self.bounds.centroid())
302 refix_vec = (fix - pos) + jitter_vec
303 self.bounds.translate(*refix_vec)
304
312
313
319
320
349