"""
A family of function objects for transforming one set of coordinates into
another.
Coordinate mapper functions are useful for defining magnifications and
other kinds of transformations on sheet coordinates, e.g. for defining
retinal magnification using a CFProjection. A CoordinateMapperFn
(e.g. MagnifyingMapper), is applied to an (x,y) pair and returns a new
(x,y) pair. To apply a mapping to a CF projection, set the
CFProjection's coord_mapper parameter to an instance of the desired
CoordinateMapperFn.
"""
from math import atan,pi,atan2
from numpy import exp,log,sqrt,sin,cos,ones,dot
from numpy.matlib import matrix
import param
from topo.base.functionfamily import CoordinateMapperFn
from topo.misc.util import signabs
from topo import numbergen
[docs]class ConstantMapper(CoordinateMapperFn):
"""
Map all values to the same constant, pre-specified coordinates.
"""
x_cons = param.Number(default=0.0,doc="""
Constant x value returned by the mapping.""")
y_cons = param.Number(default=0.0, doc="""
Constant y value returned by the mapping.""")
def __call__(self, x, y):
# Ignores all (x,y), always returning (x_cons,y_cons)
return self.x_cons, self.y_cons
[docs]class Pipeline(CoordinateMapperFn):
"""
Applies a sequence of coordmappers, left to right.
"""
mappers=param.List(default=[],
doc="The sequence of mappers to apply.")
def __call__(self,x,y):
return reduce( lambda args,f: apply(f,args),
[(x,y)] + self.mappers )
[docs]class Jitter(CoordinateMapperFn):
"""
Additively modifies calculated x,y coordinates, e.g. with random noise.
"""
scale = param.Parameter(default=0.0,doc="Amount of jitter.")
gen = param.Parameter(default=numbergen.UniformRandom(),doc=
"Number generator to use, typically a random distribution.")
def __call__(self,x,y):
return x+(self.gen()-0.5)*self.scale,y+(self.gen()-0.5)*self.scale
[docs]class NormalJitter(CoordinateMapperFn):
"""
Additively modifies calculated x,y coordinates with Gaussian random noise.
"""
gen = param.Parameter(default=numbergen.NormalRandom(),doc=
"Number generator to use, typically a Gaussian distribution.")
def __call__(self,x,y):
return x+self.gen(),y+self.gen()
[docs]class Grid(CoordinateMapperFn):
"""
Divides the 2D area into a grid, where all points within each grid
element map to the same location.
"""
xdensity = param.Number(default=1, bounds=(0,None), doc="""
Number of columns per 1.0 input sheet distance horizontally.""")
ydensity = param.Number(default=1, bounds=(0,None), doc="""
Number of rows per 1.0 input sheet distance vertically.""")
def __call__(self,x,y):
xd=self.xdensity
yd=self.ydensity
xquant=(1.0/xd)*(int(xd*(x+0.5))-(0.5*(xd-1)))
yquant=(1.0/yd)*(int(yd*(y+0.5))-(0.5*(yd-1)))
return xquant,yquant
[docs]class Polar2Cartesian(CoordinateMapperFn):
"""
Map from polar (radius,angle) to Cartesian (x,y) coordinates.
"""
degrees=param.Boolean(default=True,
doc="Indicates whether the input angle is in degrees or radians.")
def __call__(self, r, theta):
if self.degrees:
theta = theta * pi/180
return r*cos(theta), r*sin(theta)
[docs]class Cartesian2Polar(CoordinateMapperFn):
"""
Maps from Cartesian (x,y) to polar (radius,angle).
"""
degrees = param.Boolean(default=True,
doc="Indicates whether the output angle is in degrees or radians.")
negative_radii = param.Boolean(default = False,
doc="""If true, coordinates with negative x values will be
given negative radii, and angles between -90 and 90
degrees. (useful for mapping to saccade amplitude/direction space)""")
def __call__(self, x, y):
if self.negative_radii:
xsgn,xabs = signabs(x)
radius = xsgn * sqrt(x*x+y*y)
angle = atan2(y,xabs)
else:
radius = sqrt(x*x+y*y)
angle = atan2(y,x)
if self.degrees:
angle *= 180/pi
return radius,angle
def Translate2dMat(xoff,yoff):
"""
Return an affine transformation matrix that translates points by
the offset (xoff,yoff).
"""
return matrix([[1, 0, xoff],
[0, 1, yoff],
[0, 0, 1 ]])
def Rotate2dMat(t):
"""
Return an affine transformation matrix that rotates the points
around the origin by t radians.
"""
return matrix([[cos(t), -sin(t), 0],
[sin(t), cos(t), 0],
[ 0 , 0 , 1]])
def Scale2dMat(sx,sy):
"""
Return an affine translation matrix that scales the points
toward/away from the origin by a factor of sx on x-axis and sy on
the y-axis.
"""
return matrix([[sx, 0, 0],
[ 0, sy, 0],
[ 0, 0, 1]])
[docs]class Translate2d(AffineTransform):
"""
Translate the input by xoff,yoff.
"""
xoff = param.Number(default=0.0)
yoff = param.Number(default=0.0)
def __init__(self,**kw):
super(Translate2d,self).__init__(**kw)
self.matrix = Translate2dMat(self.xoff,self.yoff)
[docs]class Rotate2d(AffineTransform):
"""
Rotate the input around the origin by an angle in radians.
"""
angle = param.Number(default=0.0)
def __init__(self,**kw):
super(Rotate2d,self).__init__(**kw)
self.matrix = Rotate2dMat(self.angle)
[docs]class Scale2d(AffineTransform):
"""
Scale the input along the x and y axes by sx and sy, respectively.
"""
sx = param.Number(default=1.0)
sy = param.Number(default=1.0)
def __init__(self,**kw):
super(Scale2d,self).__init__(**kw)
self.matrix = Scale2dMat(self.sx,self.sy)
[docs]class SingleDimensionMapper(CoordinateMapperFn):
"""
Coordinate Mapper that uses an origin-centered 1-D mapping function.
An abstract mapping function for coordinate mappers that remap
based on the radius, x, or y individually. Subclasses should override
_map_fn(self,z).
"""
__abstract = True
in_range = param.Number(default=0.5*sqrt(2),bounds=(0,None),doc="""
The maximum range of the mapping input.""")
out_range = param.Number(default=0.5*sqrt(2),bounds=(0,None), doc="""
The maximum range of the output.""")
remap_dimension = param.ObjectSelector(default='radius',
objects=['radius','x','y','xy'],doc="""
The dimension to remap. ('xy' remaps x and y independently.)""")
def __call__(self,x,y):
if self.remap_dimension == 'radius':
r = sqrt(x**2 + y**2)
a = atan2(x,y)
new_r = self._map_fn(r)
xout = new_r * sin(a)
yout = new_r * cos(a)
else:
if 'x' in self.remap_dimension:
xout = self._map_fn(x)
else:
xout = x
if 'y' in self.remap_dimension:
yout = self._map_fn(y)
else:
yout = y
return xout,yout
def _map_fn(self,z):
raise NotImplementedError
[docs]class MagnifyingMapper(SingleDimensionMapper):
"""
Exponential (magnifying) mapping function.
Provides a mapping that magnifies the center of the activity image.
Parameter k indicates amount of magnification, where 0 means no
magnification.
"""
k = param.Number(default=1.0,bounds=(0,None))
def _map_fn(self,z):
k = self.k
if k == 0:
return z
else:
sgn,z = signabs(z)
return sgn * self.out_range * (exp(z/self.in_range*k)-1)/(exp(k)-1)
[docs]class ReducingMapper(SingleDimensionMapper):
"""
Provides a mapping that reduces the center of the activity.
Roughly the inverse of MagnifyingMapper. k indicates amount of reduction.
"""
k = param.Number(default=1.0,bounds=(0,None))
def _map_fn(self,z):
k = self.k
sgn,z = signabs(z)
return sgn * self.out_range * log(z/self.in_range*k+1)/log(k+1)
[docs]class OttesSCMapper(CoordinateMapperFn):
"""
Abstract class for superior colliculus mappings.
Subclasses of this class implement afferent and efferent mappings
from Ottes et al. (1986) Vision Research 26:857-873.
Default constant values are from Table 1, ibid.
"""
__abstract = True
A = param.Number(default=5.3, doc="""
Shape parameter A, in degrees""")
Bu = param.Number(default=1.8, doc="""
Rostral-caudal scale parameter, in mm""")
Bv = param.Number(default=1.8, doc="""
Orthogonal (medial-lateral?) scale paraemter in mm/deg""")
mm_scale = param.Number(default=8.0,doc="""
Scale factor to convert constants Bu and Bv from mm to sheet
units. Expressed in mm/unit. """)
amplitude_scale = param.Number(default=1,doc="""
Scale factor for saccade command amplitude, expressed in
degrees per unit of sheet. Indicates how large a
saccade is represented by the x-component of the command
input.""")
direction_scale = param.Number(default=1,doc="""
Scale factor for saccade command direction, expressed in
degrees per unit of sheet. Indicates what direction of saccade
is represented by the y-component of the command input.""")
def __call__(self,x,y):
raise NotImplementedError
[docs]class OttesSCMotorMapper(OttesSCMapper):
"""
Efferent superior colliculus mapping.
Provides the output/motor mapping from SC as defined by Ottes et al.
(see superclass docs for reference)
The mapping allows the creation of a single sheet representing
both colliculi, one in the x-positive hemisheet and the other in
the x-negative hemisheet.
"""
def __call__(self,x,y):
A = self.A
Bu = self.Bu / self.mm_scale
Bv = self.Bv / self.mm_scale
R = x * self.amplitude_scale
phi = y * self.direction_scale
Rsign,R = signabs(R)
u,v = ottes_mapping(R,phi,A,Bu,Bv)
return Rsign*u,v
[docs]class OttesSCSenseMapper(OttesSCMapper):
"""
Afferent superior colliculus mapping.
Provides the input/retinal mapping to SC as defined by Ottes et al.
(see superclass docs for reference)
The mapping allows the creation of a single sheet representing
both colliculi, one in the x-positive hemisheet and the other in
the x-negative hemisheet.
[NOTE: see warning in docs for ottes_inverse_mapping().]
"""
def __call__(self,x,y):
A = self.A
Bu = self.Bu
Bv = self.Bv
u = x * self.mm_scale
v = y * self.mm_scale
usgn,u = signabs(u)
R,phi = ottes_inverse_mapping(u,v,A,Bu,Bv)
return (usgn*R/self.amplitude_scale,
phi/self.direction_scale)
def ottes_mapping(R,phi,A,Bu,Bv):
"""
The efferent mapping function from Ottes et al. (1986)
Vision Research 26:857-873.
Takes saccade with amplitude R (in degrees) and direction
phi (in degrees), and returns a location u,v on the colliculus
in mm, where the u axis is rostral/caudal, and the v axis is
medial/lateral.
"""
phi *= pi/180
u = Bu * (log(sqrt(R**2 + A**2 + 2*A*R*cos(phi))) - log(A))
v = Bv * atan((R*sin(phi))/(R*cos(phi)+A))
return u,v
def ottes_inverse_mapping(u,v,A,Bu,Bv):
"""
Approximate inverse of ottes_mapping(), using the inverse function
provided in the appendix of Ottes et al. (1986) Vision Research 26:857-873
Takes takes a location u,v on the colliculus in mm and maps
to a retinal eccentricity R and direction phi, in degrees.
Inverse is approximate, with increasing error as positions near the
edges of the collicular sheet. (I.e. with high absolute v value).
"""
rads = pi/180
R = A * sqrt(exp(2*u/Bu) - 2*exp(u/Bu)*cos(rads*v/Bv) + 1)
#phi = atan( (exp(u/Bu)*sin(rads*v/Bv)) / (exp(u/Bu)*cos(rads*v/Bv) -1) )
phi = atan2( (exp(u/Bu)*sin(rads*v/Bv)), (exp(u/Bu)*cos(rads*v/Bv) -1) ) * 180/pi
# JPALERT: Don't know why we have to multiply by 180/pi twice, but the answers
# are way off without it. Is the bug in my code, or in the original formula?
return R,phi*180/pi
# JPALERT: Temporary testing function. Will disappear eventually.
def test_ottes_inverse():
A,Bu,Bv = 5.3,1.8,1.8
print '%10s %10s | %10s %10s | %10s %10s | %s' \
% ('R in','phi in','R out','phi out','R err','phi err','phi ratio')
for r in range(10,60,10):
for phi in range(-60,60,10):
if phi != 0:
u,v = ottes_mapping(r,phi,A,Bu,Bv)
r2,phi2 = ottes_inverse_mapping(u,v,A,Bu,Bv)
print '%10.2f %10.2f | %10.2f %10.2f | %10.2f %10.2f | %.2f' \
% (r,phi,r2,phi2,r2-r,phi2-phi,phi/phi2)
_public = list(set([_k for _k,_v in locals().items()
if isinstance(_v,type) \
and issubclass(_v,CoordinateMapperFn)]))
# Automatically discover all .py files in this directory.
import os,fnmatch
__all__ = _public + [f.split('.py')[0] for f in os.listdir(__path__[0]) if fnmatch.fnmatch(f,'[!._]*.py')]
del f,os,fnmatch