Accompanying Readings: Chapter 8
As written, this is clearly non-sensical
But don't forget about time
In [1]:
%pylab inline
In [12]:
import nengo
model = nengo.Network()
with model:
ensA = nengo.Ensemble(100, dimensions=1)
def feedback(x):
return x+1
conn = nengo.Connection(ensA, ensA, function=feedback, synapse = 0.1)
ensA_p = nengo.Probe(ensA, synapse=.01)
sim = nengo.Simulator(model)
sim.run(.5)
plot(sim.trange(), sim.data[ensA_p])
ylim(-1.5,1.5);
What are the precise dynamics?
What about $f(x)=-x$?
In [13]:
with model:
def feedback(x):
return -x
conn.function = feedback
sim = nengo.Simulator(model)
sim.run(.5)
plot(sim.trange(), sim.data[ensA_p])
ylim(-1.5,1.5);
In [14]:
from nengo.utils.functions import piecewise
with model:
stim = nengo.Node(piecewise({0:1, .2:-1, .4:0}))
nengo.Connection(stim, ensA)
sim = nengo.Simulator(model)
sim.run(.6)
plot(sim.trange(), sim.data[ensA_p])
ylim(-1.5,1.5);
Does this make sense?
What about $f(x)=x^2$?
In [27]:
with model:
stim.output = piecewise({.1:.2, .2:.4, .4:0})
def feedback(x):
return x*x
conn.function = feedback
sim = nengo.Simulator(model)
sim.run(.5)
plot(sim.trange(), sim.data[ensA_p])
ylim(-1.5,1.5);
In [21]:
import nengo
from nengo.utils.functions import piecewise
model = nengo.Network(seed=4)
with model:
stim = nengo.Node(piecewise({.3:1}))
ensA = nengo.Ensemble(100, dimensions=1)
def feedback(x):
return x
nengo.Connection(stim, ensA)
#conn = nengo.Connection(ensA, ensA, function=feedback)
stim_p = nengo.Probe(stim)
ensA_p = nengo.Probe(ensA, synapse=.1)
sim = nengo.Simulator(model)
sim.run(1)
plot(sim.trange(), sim.data[ensA_p], label="$\hat{x}$")
plot(sim.trange(), sim.data[stim_p], label="$x$")
legend()
ylim(-.2,1.5);
In [22]:
with model:
ensA_p = nengo.Probe(ensA, synapse=.03)
sim = nengo.Simulator(model)
sim.run(1)
plot(sim.trange(), sim.data[ensA_p], label="$\hat{x}$")
plot(sim.trange(), sim.data[stim_p], label="$x$")
legend()
ylim(-.2,1.5);
In [25]:
tau = 0.03
with model:
ensA_p = nengo.Probe(ensA, synapse=tau)
sim = nengo.Simulator(model)
sim.run(1)
stim_filt = nengo.synapses.filt(sim.data[stim_p], synapse=tau, dt=sim.dt)
plot(sim.trange(), sim.data[ensA_p], label="$\hat{x}$")
plot(sim.trange(), sim.data[stim_p], label="$x$")
plot(sim.trange(), stim_filt, label="$h(t)*x(t)$")
legend()
ylim(-.2,1.5);
So what does a recurrent connection do?
where $$ h(t) = \begin{cases} e^{-t/\tau} &\mbox{if } t > 0 \\ 0 &\mbox{otherwise} \end{cases} $$
How can we work with this?
General rule of thumb: convolutions are annoying, so let's get rid of them
$X(s)=F(s){1 \over {1+s\tau}}$
$X(s)(1+s\tau) = F(s)$
$X(s) + X(s)s\tau = F(s)$
$sX(s) = {1 \over \tau} (F(s)-X(s))$
${dx \over dt} = {1 \over \tau} (f(x(t))-x(t))$
This says that if we introduce a recurrent connection, we end up implementing a differential equation
So what happened with $f(x)=x+1$?
And $f(x)=x^2$?
What if there's some differential equation we really want to implement?
What happens if there's an input as well?
Follow the same derivation steps
So if you have some input that you want added to $\dot{x}$, you need to scale it by $\tau$
This lets us do any differential equation of the form $\dot{x}=f(x)+g(u)$
$\dot{x}(t) = A x(t) + B u(t)$
Our goal is to convert this to a structure which has $h(t)$ as the transfer function instead of the standard $\int$
Using Laplace on the standard form gives:
$sX(s) = A X(s) + B U(s)$
$X(s) = {1 \over {1 + s\tau}} (A'X(s) + B'U(s))$
$X(s) + \tau sX(s) = (A'X(s) + B'U(s))$
$sX(s) = {1 \over \tau} (A'X(s) + B'U(s) - X(s))$
$sX(s) = {1 \over \tau} ((A' - 1) X(s) + B'U(s))$
$A' = \tau A + I$ and
$B' = \tau B$
where $I$ is the identity matrix
This is nice because lots of engineers think of the systems they build in these terms (i.e. as linear control systems).
$\dot{x}(t) = F(x(t),u(t),t)$
$X(s) = H(s)F'(X(s),U(s),s)$
$X(s) = {1 \over {1 + s\tau}} F'(X(s),U(s),s)$
$sX(s) = {1 \over \tau} (F'(X(s),U(s),s) - X(s))$
$F'(X(s),U(s),s) = \tau(F(X(s),U(s),s)) + X(s)$
$\dot{x}=v$
It's a linear system, so, to get it in the standard control form $\dot{x}=Ax+Bu$ we have:
In [58]:
import nengo
from nengo.utils.functions import piecewise
tau = 0.01
model = nengo.Network('Eye control', seed=4)
with model:
stim = nengo.Node(piecewise({.3:1, .6:0 }))
velocity = nengo.Ensemble(100, dimensions=1)
position = nengo.Ensemble(100, dimensions=1)
def feedback(x):
return 1*x
conn = nengo.Connection(stim, velocity)
conn = nengo.Connection(velocity, position, transform=tau, synapse=tau)
conn = nengo.Connection(position, position, function=feedback, synapse=tau)
stim_p = nengo.Probe(stim)
position_p = nengo.Probe(position, synapse=.01)
velocity_p = nengo.Probe(velocity, synapse=.01)
sim = nengo.Simulator(model)
sim.run(1)
plot(sim.trange(), sim.data[stim_p], label = "stim")
plot(sim.trange(), sim.data[position_p], label = "position")
plot(sim.trange(), sim.data[velocity_p], label = "velocity")
legend(loc="best");
What will they do?
### Distortion error
What affects this?
In [19]:
import nengo
from nengo.dists import Uniform
from nengo.utils.ensemble import tuning_curves
model = nengo.Network(label='Neurons')
with model:
neurons = nengo.Ensemble(200, dimensions=1, max_rates=Uniform(100,200))
connection = nengo.Connection(neurons, neurons)
sim = nengo.Simulator(model)
d = sim.data[connection].weights.T
x, A = tuning_curves(neurons, sim)
xhat = numpy.dot(A, d)
plot(x, xhat-x)
axhline(0, color='k')
xlabel('$x$')
ylabel('$\hat{x}-x$');
$\dot{x}=-{1 \over \tau_c}x + v$
$\tau_c$ is the time constant of that return to centre
$A'=\tau {-1 \over \tau_c}+1$
In [66]:
import nengo
from nengo.utils.functions import piecewise
tau = 0.1
tau_c = 2.0
model = nengo.Network('Eye control', seed=5)
with model:
stim = nengo.Node(piecewise({.3:1, .6:0 }))
velocity = nengo.Ensemble(100, dimensions=1)
position = nengo.Ensemble(200, dimensions=1)
def feedback(x):
return (-tau/tau_c + 1)*x
conn = nengo.Connection(stim, velocity)
conn = nengo.Connection(velocity, position, transform=tau, synapse=tau)
conn = nengo.Connection(position, position, function=feedback, synapse=tau)
stim_p = nengo.Probe(stim)
position_p = nengo.Probe(position, synapse=.01)
velocity_p = nengo.Probe(velocity, synapse=.01)
sim = nengo.Simulator(model)
sim.run(5)
plot(sim.trange(), sim.data[stim_p], label = "stim")
plot(sim.trange(), sim.data[position_p], label = "position")
plot(sim.trange(), sim.data[velocity_p], label = "velocity")
legend(loc="best");
$\dot{x} = -d x + v$
This is no longer in the standard $Ax + Bu$ form. Sort of...
We need to compute a nonlinear function of an input ($d$) and the state variable ($x$)
In [4]:
import nengo
from nengo.utils.functions import piecewise
tau = 0.1
model = nengo.Network('Controlled integrator', seed=1)
with model:
vel = nengo.Node(piecewise({.2:1.5, .5:0 }))
dec = nengo.Node(piecewise({.7:.2, .9:0 }))
velocity = nengo.Ensemble(100, dimensions=1)
decay = nengo.Ensemble(100, dimensions=1)
position = nengo.Ensemble(400, dimensions=2)
def feedback(x):
return -x[1]*x[0]+x[0], 0
conn = nengo.Connection(vel, velocity)
conn = nengo.Connection(dec, decay)
conn = nengo.Connection(velocity, position[0], transform=tau, synapse=tau)
conn = nengo.Connection(decay, position[1], synapse=0.01)
conn = nengo.Connection(position, position, function=feedback, synapse=tau)
position_p = nengo.Probe(position, synapse=.01)
velocity_p = nengo.Probe(velocity, synapse=.01)
decay_p = nengo.Probe(decay, synapse=.01)
sim = nengo.Simulator(model)
sim.run(1)
plot(sim.trange(), sim.data[decay_p])
lineObjects = plot(sim.trange(), sim.data[position_p])
plot(sim.trange(), sim.data[velocity_p])
legend(('decay','position','decay','velocity'),loc="best");
In [5]:
from nengo_gui.ipython import IPythonViz
IPythonViz(model, "controlled_integrator.py.cfg")
In [95]:
import nengo
model = nengo.Network('Oscillator')
freq = 1
with model:
stim = nengo.Node(lambda t: [.5,.5] if t<.02 else [0,0])
osc = nengo.Ensemble(200, dimensions=2)
def feedback(x):
return x[0]+freq*x[1], -freq*x[0]+x[1]
nengo.Connection(osc, osc, function=feedback, synapse=.01)
nengo.Connection(stim, osc)
osc_p = nengo.Probe(osc, synapse=.01)
sim = nengo.Simulator(model)
sim.run(.5)
figure(figsize=(12,4))
subplot(1,2,1)
plot(sim.trange(), sim.data[osc_p]);
xlabel('Time (s)')
ylabel('State value')
subplot(1,2,2)
plot(sim.data[osc_p][:,0],sim.data[osc_p][:,1])
xlabel('$x_0$')
ylabel('$x_1$');
In [84]:
from nengo_gui.ipython import IPythonViz
IPythonViz(model, "oscillator.py.cfg")
Lorenz Attractor (a chaotic attractor)
In [85]:
import nengo
model = nengo.Network('Lorenz Attractor', seed=3)
tau = 0.1
sigma = 10
beta = 8.0/3
rho = 28
def feedback(x):
dx0 = -sigma * x[0] + sigma * x[1]
dx1 = -x[0] * x[2] - x[1]
dx2 = x[0] * x[1] - beta * (x[2] + rho) - rho
return [dx0 * tau + x[0],
dx1 * tau + x[1],
dx2 * tau + x[2]]
with model:
lorenz = nengo.Ensemble(2000, dimensions=3, radius=60)
nengo.Connection(lorenz, lorenz, function=feedback, synapse=tau)
lorenz_p = nengo.Probe(lorenz, synapse=tau)
sim = nengo.Simulator(model)
sim.run(14)
figure(figsize=(12,4))
subplot(1,2,1)
plot(sim.trange(), sim.data[lorenz_p]);
xlabel('Time (s)')
ylabel('State value')
subplot(1,2,2)
plot(sim.data[lorenz_p][:,0],sim.data[lorenz_p][:,1])
xlabel('$x_0$')
ylabel('$x_1$');
In [86]:
from nengo_gui.ipython import IPythonViz
IPythonViz(model, "lorenz.py.cfg")
In [87]:
import nengo
model = nengo.Network('Square Oscillator')
tau = 0.02
r=4
def feedback(x):
if abs(x[1])>abs(x[0]):
if x[1]>0: dx=[r, 0]
else: dx=[-r, 0]
else:
if x[0]>0: dx=[0, -r]
else: dx=[0, r]
return [tau*dx[0]+x[0], tau*dx[1]+x[1]]
with model:
stim = nengo.Node(lambda t: [.5,.5] if t<.02 else [0,0])
square_osc = nengo.Ensemble(1000, dimensions=2)
nengo.Connection(square_osc, square_osc, function=feedback, synapse=tau)
nengo.Connection(stim, square_osc)
square_osc_p = nengo.Probe(square_osc, synapse=tau)
sim = nengo.Simulator(model)
sim.run(2)
figure(figsize=(12,4))
subplot(1,2,1)
plot(sim.trange(), sim.data[square_osc_p]);
xlabel('Time (s)')
ylabel('State value')
subplot(1,2,2)
plot(sim.data[square_osc_p][:,0],sim.data[square_osc_p][:,1])
xlabel('$x_0$')
ylabel('$x_1$');
r
)?
In [88]:
import nengo
model = nengo.Network('Heart Oscillator')
tau = 0.02
r=4
def feedback(x):
return [-tau*r*x[1]+x[0], tau*r*x[0]+x[1]]
def heart_shape(x):
theta = np.arctan2(x[1], x[0])
r = 2 - 2 * np.sin(theta) + np.sin(theta)*np.sqrt(np.abs(np.cos(theta)))/(np.sin(theta)+1.4)
return -r*np.cos(theta), r*np.sin(theta)
with model:
stim = nengo.Node(lambda t: [.5,.5] if t<.02 else [0,0])
heart_osc = nengo.Ensemble(1000, dimensions=2)
heart = nengo.Ensemble(100, dimensions=2, radius=4)
nengo.Connection(stim, heart_osc)
nengo.Connection(heart_osc, heart_osc, function=feedback, synapse=tau)
nengo.Connection(heart_osc, heart, function=heart_shape, synapse=tau)
heart_p = nengo.Probe(heart, synapse=tau)
sim = nengo.Simulator(model)
sim.run(4)
figure(figsize=(12,4))
subplot(1,2,1)
plot(sim.trange(), sim.data[heart_p]);
xlabel('Time (s)')
ylabel('State value')
subplot(1,2,2)
plot(sim.data[heart_p][:,0],sim.data[heart_p][:,1])
xlabel('$x_0$')
ylabel('$x_1$');
In [ ]:
import nengo
from nengo.utils.functions import piecewise
model = nengo.Network('Controlled Oscillator')
tau = 0.1
freq = 20
def feedback(x):
return x[1]*x[2]*freq*tau+1.1*x[0], -x[0]*x[2]*freq*tau+1.1*x[1], 0
with model:
stim = nengo.Node(lambda t: [20,20] if t<.02 else [0,0])
freq = nengo.Node(piecewise({0:1, 2:.5, 6:-1}))
ctrl_osc = nengo.Ensemble(500, dimensions=3)
nengo.Connection(ctrl_osc, ctrl_osc, function=feedback, synapse=tau)
nengo.Connection(stim, ctrl_osc[0:2])
nengo.Connection(freq, ctrl_osc[2])
ctrl_osc_p = nengo.Probe(ctrl_osc, synapse=0.01)
sim = nengo.Simulator(model)
sim.run(8)
figure(figsize=(12,4))
subplot(1,2,1)
plot(sim.trange(), sim.data[ctrl_osc_p]);
xlabel('Time (s)')
ylabel('State value')
subplot(1,2,2)
plot(sim.data[ctrl_osc_p][:,0],sim.data[ctrl_osc_p][:,1])
xlabel('$x_0$')
ylabel('$x_1$');
In [ ]:
from nengo_gui.ipython import IPythonViz
IPythonViz(model, "controlled_oscillator.py.cfg")