Package topo :: Package sheet :: Module saccade
[hide private]
[frames] | no frames]

Source Code for Module topo.sheet.saccade

  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  # JPALERT: The next three functions (activity_centroid, 
 38  # activity_sample, and activity_mode) could actually apply to any 
 39  # Sheet.  Maybe they should be moved to topo.base.sheet? 
 40   
41 -def activity_centroid(sheet,activity=None,threshold=0.0):
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 ## Optimization to only compute centroid from 60 ## active (non-zero) units. 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
67 -def activity_sample(sheet,activity=None):
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
83 -def activity_mode(sheet,activity=None):
84 """ 85 Returns the sheet coordinates of the mode (highest value) of 86 the sheet activity. 87 """ 88 89 # JPHACKALERT: The mode is computed using numpy.argmax, and 90 # thus for distributions with multiple equal-valued modes, the 91 # result will have a systematic bias toward higher x and lower 92 # y values. (in that order). Function may still be useful for 93 # unimodal activity distributions, or sheets without limiting/squashing 94 # output functions. 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
103 -class SaccadeController(CFSheet):
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 # JPALERT: amplitude_scale and direction scale can be implemented as 130 # part of self.command_mapper, so these should probably be removed. 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
156 - def activate(self):
157 super(SaccadeController,self).activate() 158 159 # get the input projection activity 160 # decode the input projection activity as a command 161 xa,ya = self.decode_fn(self) 162 self.verbose("Saccade command centroid = (%.2f,%.2f)"%(xa,ya)) 163 164 xa,ya = self.command_mapper(xa,ya) 165 166 amplitude = xa * self.amplitude_scale 167 direction = ya * self.direction_scale 168 169 self.verbose("Saccade amplitute = %.2f."%amplitude) 170 self.verbose("Saccade direction = %.2f."%direction) 171 172 self.send_output(src_port='Saccade',data=(amplitude,direction))
173 174 175 176 177
178 -class ShiftingGeneratorSheet(SequenceGeneratorSheet):
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
208 - def __init__(self,**params):
209 super(ShiftingGeneratorSheet,self).__init__(**params) 210 self.fixation_point = self.bounds.centroid()
211
212 - def start(self):
213 super(ShiftingGeneratorSheet,self).start() 214 if self.fixation_jitter_period > 0: 215 now = self.simulation.time() 216 refix_event = PeriodicEventSequence(now+self.fixation_jitter_period, 217 self.fixation_jitter_period, 218 [FunctionEvent(0,self.refixate)]) 219 self.simulation.enqueue_event(refix_event)
220
221 - def input_event(self,conn,data):
222 if conn.dest_port == 'Saccade': 223 # the data should be (amplitude,direction) 224 amplitude,direction = data 225 self.shift(amplitude,direction)
226
227 - def generate(self):
228 super(ShiftingGeneratorSheet,self).generate() 229 self.send_output(src_port='Position', 230 data=self.bounds.aarect().centroid())
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 # JPALERT: Right now this method assumes that we're doing 250 # colliculus-style saccades. i.e. amplitude and direction 251 # relative to the current position. Technically it would 252 # not be hard to also support absolute or relative x,y 253 # commands, and select what style to use with either with 254 # a parameter, or with a different input port (e.g. 'xy 255 # relative', 'xy absolute' etc. 256 257 # JPHACKALERT: Currently there is no support for modeling the 258 # fact that saccades actually take time, and larger amplitudes 259 # take more time than small amplitudes. No clue if we should 260 # do that, or how, or what gets sent out while the saccade 261 # "eye" is moving. 262 263 assert not self._out_of_bounds() 264 265 # convert the command to x/y translation 266 radius = amplitude/self.visual_angle_scale 267 268 # if the amplitude is negative, negate the direction (so up is still up) 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
285 - def refixate(self):
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
305 - def _translate(self,radius,angle):
306 angle *= pi/180 307 xoff = radius * cos(angle) 308 yoff = radius * sin(angle) 309 310 self.verbose("Applying translation vector (%.2f,%.2f)"%(xoff,yoff)) 311 self.bounds.translate(xoff,yoff)
312 313
314 - def _out_of_bounds(self):
315 """ 316 Return true if the centroid of the current bounds is outside the saccade bounds. 317 """ 318 return not self.saccade_bounds.contains(*self.bounds.aarect().centroid())
319 320
321 - def _find_saccade_in_bounds(self,radius,theta):
322 """ 323 Find a saccade in the given direction (theta) that lies within self.saccade_bounds. 324 325 Assumes that the given saccade was already performed and 326 landed out of bounds. 327 """ 328 329 # JPHACKALERT: This method iterates to search for a saccade 330 # that lies in bounds along the saccade vector. We should 331 # really compute this algebraically. Doing so involves computing 332 # the intersection of the saccade vector with the saccade 333 # bounds. Ideally, each type of BoundingRegion would know how 334 # to compute its own intersection with a given line (should be 335 # easy for boxes, circles, and ellipses, at least.) 336 337 # Assume we're starting out of bounds, so start by shifting 338 # back to the original position 339 self._translate(-radius,theta) 340 341 while not self._out_of_bounds(): 342 radius *= 0.5 343 self._translate(radius,theta) 344 345 radius = -radius 346 while self._out_of_bounds(): 347 radius *= 0.5 348 self._translate(radius,theta)
349