/
elastica_neurons.py
292 lines (236 loc) · 10.5 KB
/
elastica_neurons.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
'''
This file containts the scripts for contour generation under the elastica theory.
The elastica energy is estimated using the integration method outlined in Sharon et al.
I.e. it's assumed to be a polynomial f = function, and
author: swkeemink@scimail.eu
'''
from __future__ import division
import holoviews as hv
import pylab as pl
from pylab import exp,cos,sin,pi,tan
def mises(a,k,ref,x):
''' basic von mises function
Inputs
-----------------------
- a: determines magnitude
- k: determines width (low k is wider width)
- ref: reference angle
- x: input angle
'''
return a*exp(k*(cos(2*(ref-x))))
class scene:
""" Scene class. Define a scene, for which the neural responses to each bar can then be found """
def __init__(self,N,O,X,dim,Kc,Ac,a,E0):
self.n = len(O) # number of bars
self.N = N # number of neurons per location
self.O = O # bar orientations
self.X = X # bar locations
self.dim = dim # image dimension [x,y]
self.ang = pl.linspace(-pi/2,pi/2-pi/N,N) # orientations
self.Kc = Kc # determines width of tuning to bars in location (lower Kc is wider tuning)
self.Ac = Ac # determines response magnitude
self.a = a # determines strength of modulation
self.E0 = E0 # offset for modulation
self.FRc = pl.zeros((self.n,N)) # 'firing rates' or 'probabilities' for local bars
self.FRs = pl.zeros((self.n,N)) # modulation from smoothest targets (net effect)
self.FR = pl.zeros((self.n,N)) # final 'firing rates' or 'probabilities' for each bar
self.est = pl.zeros(self.n) # to store the orientation estimates
def popvec(self,X):
''' Population vector for the set of responses X, with each value in
the vector X corresponding to an angle in self.ang
X is a 1D vector of length len(self.ang)
Returns the angle of the population vector.
'''
# define vector coordinates
v = pl.zeros((2,self.N))
v[0,:] = cos(2*self.ang)
v[1,:] = sin(2*self.ang)
# find population vector
vest0 = pl.sum(((X-min(X))/max(X))*v,1)
# return the angle of the population vector
return 0.5*pl.arctan2(vest0[1],vest0[0])
def plotscene(self,length=1,oriens = 'NA',alphas='NA',colors='off'):
''' Plot the scene
Inputs
----------------
- length: length of each plotted bar
- oriens: orientations of each bar, if 'NA' use the orientations as defined in self.O
- alphas: the alpha values for each bar, if 'NA' set all to one
- colors: the color of each bar, if 'NA' all are black
oriens and alphas should all be arrays of length self.n if 'off'
'''
# check for alphas/oriens inputs
if alphas == 'NA': alphas = pl.ones(self.n)
if oriens == 'NA': oriens = self.O
# initiate image
img = hv.Curve([])
for i in range(self.n):
# get bar location
x = self.X[i,0]
y = self.X[i,1]
# get bar orientation
f = oriens[i]
# check colors
if colors == 'off':
c = 'k'
else:
if alphas[i]<pl.mean(alphas): # if below average make blue
c = 'b'
else: # otherwise make red
c = 'r'
# plot the bar
img *= hv.Curve(zip([x-sin(f)*length,x+sin(f)*length],[y-cos(f)*length,y+cos(f)*length]))(style={'color':c,'linewidth':4,'alpha':alphas[i]/max(alphas)})
# return img object
return img
def findE(self,c,f,X):
''' find E, the approximated sharon energy
Inputss
----------
- c: current bar orientation, relative to the vertical (can be double or array, if array D returns an array of D values)
- f: flanker orientation, relative to the vertical (double)
- X: flanker position, relative to the current bar (x,y)
So, as examples...
findE(0,0,[0,1]) would be two vertical bars, positioned vertically in a line.
findE(pi/2,pi/2,[1,0]) would be two horizontal bars, positioned horizontally in a line.
'''
# define x and y
x = X[0]
y = X[1]
# flanker positional angle
theta = pl.arctan2(x,y)
# find and return D
Ba = pl.arctan(tan(0.5*(-f+theta)))*2
Bb = pl.arctan(tan(0.5*(c-theta)))*2
return 4*(Ba**2 + Bb**2 - Ba*Bb)
def E(self,c,f,X):
''' find the direction invariant approximated sharon energy
Inputs
----------
- c: current bar orientation, relative to the vertical (can be double or array, if array D returns an array of D values)
- f: flanker orientation, relative to the vertical (double)
- X: flanker position, relative to the current bar (x,y)
So, as examples...
E(0,0,[0,1]) would be two vertical bars, positioned vertically in a line.
E(pi/2,pi/2,[1,0]) would be two horizontal bars, positioned horizontally in a line.
'''
# check if c is an array, and assign length (note, f cannot be array)
if isinstance(c,pl.ndarray):
length = len(c)
else:
length = 1
# find D candiates across all directions
E = pl.zeros((length,4))
E[:,0] = self.findE(c,f,X)
E[:,1] = self.findE(c+pi,f,X)
E[:,2] = self.findE(c,f+pi,X)
E[:,3] = self.findE(c+pi,f+pi,X)
# return the minimum energy
return E.min(1)
def plotlocalmod(self,iLoc,torus='on'):
''' Plot contributions to local modulation
iLoc: location id (which bar to look at)
torus: whether image is on a torus or not
'''
# make mask to select all locations except iLoc
mask = pl.ones(self.n)
mask[iLoc] = 0
# find orientations at all other locations
F = self.O[mask==1]
# find positions of all other locations
X = self.X[mask==1,:] ##### set up properly, now based on [x,y], should be [r,theta]
# and set coordinates to be relative to iLoc
X[:,0]-=self.X[iLoc,0] # update x coordinate to make current the center
X[:,1]-=self.X[iLoc,1] # update y coordinate to make current the center
# transform other coordinates according to torus
# does not work when there are only two bars present!
if torus == 'on':
mx = self.dim[0]
X[X[:,0]<-mx/2,0] += mx
X[X[:,1]<-mx/2,1] += mx
X[X[:,0]>mx/2,0] -= mx
X[X[:,1]>mx/2,1] -= mx
# find the distances from every other bar to the 'center'
R = pl.sqrt(X[:,0]**2+X[:,1]**2)
# initiate image
img = hv.Curve([])
# find the total modulation, and plot each individual contribution
for i in range(self.n-1):
# find curvature energy
E = self.E(self.ang,F[i],X[i,:])
# find resulting modulation
h = exp(-self.a*(E-self.E0)/R[i])
# plot the modulation curve
img *= hv.Curve(zip(self.ang/pi,h))#,alpha=0.25,label=i)
# return img
return img
def simulate(self,iLoc,torus='on'):
'''
For a location iLoc simulate the responses given the scene:
- find the orientation at iLoc to get the drive
- find the orientation and relative position of all other bars, to determine the modulation
Updates FRc, FRs, FR, S , est for iLoc
Inputs
-------------------------
- iLoc: the index of the location to simulate
- torus: if 'on', put the scene on a torus
'''
# find the orientation of the bar in iLoc
c = self.O[iLoc] # 'centre' flanker
# make mask to select all locations except iLoc
mask = pl.ones(self.n)
mask[iLoc] = 0
# find orientations at all other locations
F = self.O[mask==1]
# find positions of all other locations
X = self.X[mask==1,:] ##### set up properly, now based on [x,y], should be [r,theta]
# and set coordinates to be relative to iLoc
X[:,0]-=self.X[iLoc,0] # update x coordinate to make current the center
X[:,1]-=self.X[iLoc,1] # update y coordinate to make current the center
# transform other coordinates according to torus, if necessary
# does not work when there are only two bars present!
if torus == 'on':
mx = self.dim[0]
X[X[:,0]<-mx/2,0] += mx
X[X[:,1]<-mx/2,1] += mx
X[X[:,0]>mx/2,0] -= mx
X[X[:,1]>mx/2,1] -= mx
# find the distances from every other location to iLoc
R = pl.sqrt(X[:,0]**2+X[:,1]**2)
# find the drive from the orientation in iLoc
rc = mises(self.Ac,self.Kc,c,self.ang)
# find the modulation from all other lcoations
rs=1
for i in range(self.n-1): # for all locations except iLoc
# find D across all preferred orientations
E = self.E(self.ang,F[i],X[i,:])
# update the modulation
rs*=exp(-self.a*(E-self.E0)/R[i])
# find final response in location iLoc across preferred oreintations
rf = rc*rs
# update relevant variables
self.FRc[iLoc,:] = rc#/sum(rc)
self.FRs[iLoc,:] = rs
self.FR[iLoc,:] = rf#/sum(rf)
self.est[iLoc] = self.popvec(rf)
def simulate_all(self,torus='on'):
'''
Simulate all locations.
- torus = 'on' : turns torus behaviour on or off
'''
for i in range(self.n):
self.simulate(i,torus)
def saliency(self,base):
'''
Calculate the saliency for all location either based on:
- base = 'mean': the mean responses
- base = 'max' : the max responses
'''
sal = pl.zeros(self.n)
if base == 'mean':
for i in range(self.n):
sal[i] = pl.mean(self.FR[i,:])
self.sal = sal/sal.mean()
if base == 'max':
for i in range(self.n):
sal[i] = max(self.FR[i,:])
self.sal = sal/sal.mean()